Недавно я начал писать ассемблер с нуля, используя C ++ 17 для малоизвестной операционной системы. Дизайн ассемблера поддерживает несколько целевых архитектур, но пока мне нужно поддерживать только набор инструкций MIPS I, который оказывается чрезвычайно удобным, поскольку его легко кодировать.

Одна инструкция ассемблера MIPS состоит из мнемоники операции и необязательного списка операндов (например, addi $t1,$v0,0xFF). Правильное машинное кодирование инструкции (т. Е. Структура ее 32-х битов) диктуется типом формата инструкции, который задокументирован для всех инструкций MIPS I в Справочном руководстве по программному обеспечению семейства IDT R30xx.

Три формата кодирования инструкций MIPS

На рисунке (A.1) показано, как доступные 32 бита инструкции используются каждым из трех форматов кодирования.

Битовые поля rs, rt и rd кодируют индекс регистра ЦП (от 0 до 31).

Битовые поля immediate, target и shamt кодируют задаваемое пользователем числовое значение константы.

Битовые поля op и funct кодируют предоставленное ассемблером числовое значение константы (т. Е. Флаги), используемое ЦП для идентификации и настройки себя для инструкции.

Декларативное кодирование инструкций [C ++ 17] [constexpr if]

Чтобы сохранить все правила кодирования в одном месте, ассемблер поддерживает следующий декларативный синтаксис:

Эта карта обеспечивает функцию кодирования для каждой инструкции MIPS I. Выражения, предоставленные для значений карты, генерируют эти функции кодирования с помощью расширения шаблона.

Например, из приведенного выше фрагмента IType называет шаблон функции, который принимает 5 аргументов шаблона:

  1. Код операции. Только числовое постоянное выражение (постоянные биты).
  2. RS. Постоянные биты или контрольное значение Arg, которое сообщает шаблону функции читать значение из предоставленных пользователем операндов (5.).
  3. RT. Постоянные биты или Arg.
  4. Немедленно. Постоянные биты, или Arg.
  5. Функция синтаксического анализа операндов. Считывает предоставленную пользователем строку операндов в кортеж (подробнее об этом позже).

Создание экземпляра каждого шаблона функции (RType, IType, JType) приводит к созданию пользовательской функции, которая предоставляет постоянные части кодировки инструкции во время компиляции. Другими словами, битовые поля инструкции, которые всегда одни и те же (используемые ЦП для идентификации операции, которую он должен выполнить), передаются в эти шаблоны функций как интегральные константы (не типовые интегральные параметры шаблона). Теоретически это должно позволить компилятору комбинировать все постоянные части закодированной инструкции во время компиляции.

Единственное отклонение от этого шаблона происходит в присутствии Arg часового, который использует функцию C ++ 17, известную как оператор constexpr if, для обеспечения особого регистра.

Обратите внимание, что функция IsArgSentinel - это constexpr.

Шаблон функции, показанный выше (используемый для инструкций MIPS I-типа), использует оператор constexpr if для генерации кода для чтения значения RS из предоставленной пользователем строки операнда (это список операндов, указанный после мнемоника), тогда и только тогда, когда RS, как указано в экземпляре шаблона, является Arg дозорным.

Оператор constexpr if проверяет результат константного выражения (предоставленного в качестве его условия) и * отбрасывает * операторы невыполненной ветви. Это условие оценивается во время компиляции, что позволяет компилятору исключить неудачную ветвь из кода функции.

Если значение равно true, то утверждение-ложь отбрасывается (если присутствует), в противном случае отклоняется утверждение-истина. - cppreference.

Чтение строки операнда [C ++ 14]

Шаблоны функций инструкций (т.е. IType, RType, JType) не делают никаких предположений ни о формате строки операнда, ни о порядке (или количестве!) Содержащихся в ней операндов.

Вместо этого каждый из этих шаблонов функций принимает указатель функции (5. из приведенного выше примера) на функцию синтаксического анализа строки операнда, которая разбивает строку операнда на std::tuple соответствующих полей для формата инструкции.

Затем шаблоны функций могут просто использовать основанную на типе перегрузку std::get (C ++ 14) для получения определенных операндов:

auto& rs_operand = std::get<assembler::RS>(operands);

Это использование показано в контексте шаблона функции I-типа в предыдущем разделе.

Декларативный синтаксический анализ операндов

Чтобы подготовить эти кортежи, мы должны разбить строку операнда на ее компоненты в соответствии с правилами синтаксиса соответствующей инструкции ассемблера.

Правила синтаксиса инструкции гласят:

  1. форматирование списка операндов . Либо стиль кортежа (например, $t0,$v0,0xFF), либо стиль смещения (например, $t0,0xFF($v0)).
  2. Порядок компонентов инструкции, к которым будет привязан каждый операнд. Например, $t0,$v0,0xFF должно быть RT,RS,Immediate или RS,RT,Immediate?

Оба из этих правил также могут быть реализованы декларативно с использованием шаблонов функций (см. ITypeTuple и ITypeOffset):

Обратите внимание, что в приведенных выше примерах порядок параметров нашего шаблона (т. Е. RS, RT, Immediate) определяет порядок операндов для инструкции.

Реализация этого синтаксиса в значительной степени зависит от перегрузки типа std::get, которая позволяет нам связать I-й операнд из строки операнда с любым типом поля кортежа, предоставленным для I-го аргумента шаблона в результирующем кортеже.

Необязательные операнды [C ++ 17]

Возможно, вы заметили использование std::optional (C ++ 17) выше. Этот тип используется для представления значения, которое может присутствовать или отсутствовать. Все типы операндов оболочки расширяют std::optional:

struct RS : std::optional<std::string> { ... };

Это позволяет указывать их как std::nullopt функциями синтаксического анализа операндов, для которых применимы не все типы операндов. Например, многие инструкции I-типа допускают только два определяемых пользователем операнда:

// Example: bgez $t0,0xCF
//
// RS = $t0, Immediate = 0xCF
// Field RT does NOT come from the operand list. It must always
// be 1, as required by the bgez reference spec.
{ "bgez", IType<0b000001, Arg, 1, Arg, ITypeTuple<RS, Immediate>> }

Шаблон функции ITypeTuple с двумя аргументами шаблона обрабатывает этот случай, заполняя только два из трех полей операндов в результирующем кортеже.

Оставшееся поле (не определяемое пользователем) будет _44 _ (_ 45_ в случае bgez), хотя оно никогда не будет прочитано как таковое, поскольку не будет сгенерирован код для его чтения в соответствующем экземпляре шаблона функции IType.

Можно было бы использовать аргументы вариативного шаблона для обработки кортежей как из 2, так и из 3 элементов, используя один шаблон функции, но для простоты было решено отказаться от этого.

Дополнительная информация: `constexpr` Параметры функций

В этом посте я упомянул об использовании контрольного значения Arg, которое используется для генерации кода особого случая для чтения операндов. Возможно, в идеале мы могли бы использовать что-то безопасное по типу (возможно, std::optional) в качестве типа нашего аргумента шаблона, не являющегося типом, но это невозможно в C ++ 17.

Чтобы обойти это ограничение, можно использовать параметр шаблона type вместе с недавно введенным constexpr лямбда-выражением (C ++ 17).

Хотя этот подход не был выбран из-за сложности.

Подробное объяснение этого решения можно найти в отличном сообщении в блоге Майкла Парка по теме: Параметры функции constexpr.

использованная литература