Глава 4

Дизайн собственного LLVM бэкенда 

В этой главе обсуждается структура и дизайн собственной архитектуры ЦП, ориентированной на собственный LLVM бэкенд. В разделе 4.1 обсуждается высокоуровневая структура LLVM, а в разделе 4.2 описывается конкретная реализация собственного бэкенда.

4.1 Структура и инструменты 

LLVM отличается от большинства традиционных проектов компиляторов, потому что это не просто набор отдельных программ, а набор библиотек. Все эти библиотеки разработаны с использованием объектно-ориентированного программирования и являются расширяемыми и модульными. Это наряду с его трех этапным подходом (см. раздел 2.2.4) и с его современным дизайном кода, делает его очень привлекательной инфраструктурой компилятора для работы. В этой главе представлен собственный LLVM бэкенд, ориентированный на собственный CJG RISC-процессор, архитектура которого подробно описана в главе 3.

4.1.1 Обзор кода — генератора кода 

Генератор кода является одним из многих больших фреймворков, доступных в LLVM. Эта конкретная структура предоставляет множество классов, методов и инструментов, которые помогают преобразовать IR-код LLVM в ориентированный/машинный код [20]. Большая часть базы кода, классов и алгоритмов не зависит от назначения и может использоваться всеми конкретными встроенными ресурсами, которые реализованы в ней. Два основных ориентированных компонента, которые составляют пользовательский бэкенд, являются абстрактным описанием цели и реализацией абстрактного ориентированного описания. Эти целевые компоненты структуры необходимы для каждой конкретной архитектуры в LLVM, а генератор кода использует их по мере необходимости в процессе генерации кода.

Генератор кода разделен на несколько этапов. До этапа планирования инструкций, код организован в базовые блоки, где каждый базовый блок представлен в виде ориентированного ациклического графа (DAG). Базовый блок определяется как последовательность операторов, которые выполняются по порядку, от начала основного блока до конца без возможности ветвления, за исключением окончания[8]. DAG могут быть очень полезными структурами данных для работы с базовыми блоками потому что они обеспечивают простой способ определения, какие значения, используемые в базовом блоке, используются в последующих операциях. Любое значение, которое может использоваться в последующей операции, даже в другом базовом блоке, называют «живым» значением. Когда значение не имеет возможности быть использованным, то говорят, что это «убитое» значение.

Высокоуровневые описания этапов, составляющих генератор кода, представлены ниже:

1. Выбор инструкций — переводит LLVM IR внутри операций, которые могут быть выполнены в наборе команд задачи. Виртуальные регистры в форме SSA (промежуточное представление, используемое компиляторами, в котором каждой переменной значение присваивается лишь единожды) используются для представления назначений данных. Результатом этого этапа являются DAG, содержащие инструкции для конкретных целей (заданий).

2. Планирование инструкций — определяет необходимый порядок инструкций целевой (адресованной) машины из DAG. После определения этого порядка группа DAG преобразуется в список машинных инструкций и уничтожается.

3. Оптимизация машинных инструкций — выполняет платформенные оптимизации в списке машинных инструкций, которые могут дополнительно улучшить качество кода.

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

5. Вставка кода пролога и эпилога — обычно вставляет код, относящийся к настройке (пролог), a затем уничтожение (эпилог) кадра стека для каждого базового блока.

6. Оптимизация конечного машинного кода — выполняет все конечные целевые оптимизации, определенные бэкендом.

7. Кодовая эмиссия (создание кода для конечной платформы) — низкоуровневое представление кода из абстракций машинных инструкций, предоставляемых платформой генератора кода в целевой сборки или машинного кода. Выходные данные этого этапа обычно представляют собой текстовый файл сборки или расширяемый объектный файл в формате ELF.

4.1.2 TableGen 

Одним из инструментов LLVM, который необходим для написания абстрактного целевого (объектного) описания, является TableGen (llvm-tblgen). Этот инструмент преобразует целевой файл описания (.td) в C++ код, который используется затем при генерации кода. Его основная цель состоит в том, чтобы свести большие, утомительные описания к более мелким и гибким определениям, которыми легче управлять и легче их структурировать [21]. Основная функциональность TableGen находится в TableGen бэкенде (не путать с llvm бэкендами (генераторами кода для конкретного целевого назначения)). Эти модули отвечают за перевод целевого файлового описания в формат, который может использоваться генератором кода [22]. Генератор кода предоставляет все модули TableGen, необходимые большинству ЦП для выполнения абстрактного описания целевого объекта, однако пользовательские модули TableGen могут быть написаны и для других целей.

Тот же входной код TableGen обычно создает различные выходные зависимости от используемого TableGen бэкенда. Код TableGen, приведенный в листинге 4.1, используется для определения каждого из регистров ЦП, входящих в архитектуру CJG. Бэкенд часть AsmWriter TableGen, отвечающая за создание кода для формирования кода целевой сборки, создает C++ код , приведенный в листинге 4.2. Однако, информация о регистрах часть TableGen бэкенд, которая отвечает за создание кода для описания файла регистра в генераторе кода, генерирует код на языке C++, приведенный в листинге 4.3.

Существует много больших таблиц (например, в строке 7 листинга 4.2) и функций, которые генерируются из TableGen, чтобы помочь в разработке пользовательского интерфейса LLVM. Хотя в настоящее время TableGen отвечает за основную часть описания целевого объекта, для завершения реализации абстрактного описания целевого объекта по-прежнему требуется написать большой объема кода на языке C++. По мере развития LLVM, цель состоит в том, чтобы переместить, как можно больше целевого описания в форму TableGen [20].

Листинг 4.1: Определения наборов регистров TableGen

Листинг 4.2: Вывод TableGen AsmWriter

Листинг 4.3: Вывод Tablegen RegisterInfo

4.1.3 Clang и llc 

Clang (произносится «клэнг») — это фронтенд для LLVM, который поддерживает C, C++ и Objective C/C++ [23]. Clang отвечает за функциональность, описанную в разделе 2.2.4.1. Инструмент llc — это статический компилятор LLVM, который отвечает за функциональность, описанную в разделе 2.2.4.3. Пользовательские модули, написанные для LLVM, связаны в llc, которые затем компилируется IR-кодом LLVM для целевой сборки или машинного кода.

4.2 Внедрение пользовательских целей 

Пользовательский бэкенд LLVM наследует и расширяет многие классы LLVM. Для реализации бэкенд части LLVM большинство файлов размещаются в каталоге lib/Target/TargetName/, где TargetName — это имя целевой архитектуры, на которую ссылается LLVM. Это имя важно и должно оставаться неизменным на протяжении всей разработки бэкенд части, так как оно используется внутренними компонентами LLVM для поиска пользовательской бэкенд части. Имя для этой целевой архитектуры было выбрано, как CJG, поэтому пользовательский бэкенд находится в lib/Target/CJG/. «Точка входа» бэкенд части CJG LLVM находится в пределах CJGMCTargetDescription. Это место, где бэкенд часть зарегистрирована в LLVM TargetRegistry, так что LLVM мог найти и использовать эту бэкенд часть. График который показан на рис. 4.1 дает четкое представление о классах и файлах, которые являются частью бэкенд части CJG. В дополнение к модулям RISC-процессоров, которые в настоящее время находятся в llvm с дерева исходных текстов (а именно ARM и msp430), некоторые архитектуры использовались как примеры и как ресурсы в ходе реализации CJG бэкенда: Cpu0 [24], LEG [25], и RISC-V в [26]. Остальная часть этого раздела описывает детали реализации пользовательского CJG LLVM бэкенда.

Рисунок 4.1: График включения CJGMCTargetDesc.h

4.2.1 Абстрактное описание задачи 

Как описано в разделе 4.1.2, большая часть абстрактного описания задачи написана в формате TableGen. Основные компоненты CJG бэкенд части написана в форме TableGen, в форме сведений об регистрах, специальных операндов, в формате вызовов, инструкций и в формате полных инструкции определения. В дополнение к компонентам TableGen, есть некоторые моменты, которые должны быть написаны на C++. Эти компоненты абстрактного целевого описания описаны в следующих разделах. 

4.2.1.1  Информация о регистрах 

Информация о регистре находится в CJGRegisterInfo.td. Этот файл определяет набор регистров CJG RISC, а также различные классы регистров. Это упрощает разделение регистров, которые могут содержать только определенный тип данных (например, целочисленные классы и классы регистров с плавающей запятой). Поскольку архитектура CJG не поддерживает операции с плавающей запятой, основным классом регистра является класс регистра общего назначения. Определение этого класса приведен в листинге 4.4. Определение каждого отдельного регистра находится в этом файле и показано в листинге 4.1.

Листинг 4.4: Определение класса регистров общего назначения

4.2.1.2 Соглашение о вызовах 

Определение соглашения о вызовах описывают часть ABI, которая управляет перемещением данных между вызовами функций. Определение соглашения о вызовах определены в CJGCallingConv.td и определение соглашения о возвратных вызовах приведены в листинге 4.5. Это определение описывает, какие значения возвращает функция. Во-первых, любые 8-битные или 16-битные значения должны быть преобразованы в 32-разрядное значение. Затем первые 8 возвращаемых значений помещаются в регистры R24-R31. Все остальные возвращаемые значения будут помещены в стек данных.

Листинг 4.5: Определение соглашения о возвратных вызовах

4.2.1.3 Специальные операнды 

Существует несколько специальных типов операндов, которые должны быть определены в рамках целевой архитектуры. Есть много операндов, которые предопределены в TableGen, такие как i16imm и i32imm (определенный в include/llvm/Target/Target.td), однако, бывают случаи, когда этого недостаточно. Два примера специальных операндов, которые должны быть определены, являются операндом адреса памяти и операндом кода условия перехода. Оба этих операнда должны быть определены отдельно, поскольку они не являются стандартными размерами типа данных и должны иметь специальные методы для формирования их в сборке. Пользовательский операнд memsrc содержит регистр и немедленное значение для режима индексированной адресации (как показано в таблице 3.2). Эти определения находятся в CJGInstrInfo.td и показаны в листинге 4.6. PrintMethod и EncoderMethod определяют имена пользовательских функций C++, которые будут вызываться при формировании операнда в сборке или кодировании операнда в машинном коде.

Листинг 4.6: Определения специальных операндов

4.2.1.4 Формат инструкций 

Форматы инструкций описывают форматы слов инструкций согласно форматам, описанным в разделе 3.3 наряду с некоторыми другими важными свойствами. Эти форматы определены в CJGInstrFormats.td. Базовый класс для всех форматов команд CJG показан в листинге 4.7. Затем он расширяется в несколько других классов для каждого типа инструкции. Например, определения формата инструкции АЛУ для режимов регистр-регистр и регистр-мгновенного перехода показаны в листинге 4.8.

Листинг 4.7: определение базовой инструкции CJG

4.2.1.5 Полное определение инструкций 

Полные определение инструкций наследуется от формы класса, чтобы завершить базовый класс TableGen. Эти завершающие инструкции определены в CJGInstrInfo.td. Некоторые определения инструкций АЛУ приведены в листинге 4.9. Функциональность нескольких классов упрощает определение нескольких очень похожих инструкций

Листинг 4.8: определения формата базовых инструкций ALU

друг на друга. В этом случае инструкции АЛУ регистр-регистр (rr) и регистр-непосредственного передохода (ri) определяются в рамках мультикласса. При использовании ключевого слова defm определяются все классы в мультиклассе (например, определение инструкции ADD в строке 23 листинга 4.9 расширяется в определение инструкций ADDrr и ADDri).

Листинг 4.9: завершенные определения инструкций ALU

В дополнение к опкоду, эти определения также содержат некоторую другую чрезвычайно важную информацию для LLVM. Например, рассмотрим определение ADDri. Поля outs и ins в строках 15 и 16 листинга 4.9 описывают источник и назначение выходов и входов каждой инструкции. Строка 15 описывает, что инструкция выводит одну переменную в класс регистра GPRegs и она хранится в переменной ri класса (определенной в строке 10 листинга 4.8). В строке 16 листинга 4.9 описывается, что инструкция принимает два операнда; первый операнд принимается из класса регистра GPRegs, а второй определяется пользовательский тип операнда CJGimm16. Первый операнд хранится в переменной rj класса, а второй — в переменной rk класса. Строка 17 показывает определение сборочной строки; переменная opstr передается в класс как параметр, а переменные класса ссылаются на символ $. Строки 18 и 19 описывают шаблон инструкции. Вот как генератор кода в конечном итоге может выбрать эту инструкцию из LLVM IR. Параметр opmode передается из третьего параметра объявления defm, показанного в строке 23. Тип opnode является классом SDNode, который представляет узел в DAG, используемом для выбора инструкции (назвали SelectionDAG). В данном примере SDNode является add, который уже определен LLVM. Некоторые инструкции, однако, нуждаются в пользовательской реализации SDNode. Этот шаблон будет сопоставлен, если в SelectionDAG есть узел add с двумя операндами, где один является регистром в классе GPRegs и другая константа. Назначение узла также должно быть регистром в классе GPRegs. Еще одна деталь, которая выражается в полных определениях инструкций, — это неявное использование или определение других физических регистров в ЦП. Рассмотрим простую инструкцию по сборке

add r4, r5, r6

где r5 добавляется в r6, а результат сохраняется в r4. Говорят, что эта инструкция определяет r4 и использует r5 и r6. Поскольку все инструкции add могут изменять регистр состояния, эта инструкция также неявно определяет SR. Это выражено в TableGen с использованием Defs и implicit ключевых слов и можно увидеть в строках 5, 12 и 19 листинга 4.9. Неявное использование регистра также может быть выражено в TableGen с помощью ключевого слова Uses. Это можно увидеть в определении условной инструкции перехода. Поскольку условная инструкция перехода зависит от регистра состояния, несмотря на то, что регистр состояния не является входом в инструкцию, говорят, что он неявно использует SR. Это определение приведено в листинге 4.10. В этом списке также показано использование пользовательского класса SDNode, CJGbrcc, а также пользовательского операнда cc (определенного в листинге 4.6).

Листинг 4.10: Определение условной инструкции перехода

4.2.1.6 Дополнительное описание 

Есть дополнительные описания, которые еще не были перенесены в TableGen и должны быть реализованы в C++ . Одним из таких примеров является структура CJGRegisterInfo. Зарезервированные регистры ЦП должны быть описаны функцией getReservedRegs. Код этой функции приведен в листинге 4.11

4.2.2 Выбор инструкции 

Этап выбора инструкций бэкенд части отвечает за перевод IR-кода LLVM в специализированные машинные инструкции [20]. В этом разделе описываются фазы выбора инструкций.

Листинг 4.11: Реализация описания зарезервированных регистров

4.2.2.1 Конструкция SelectionDAG 

Первым шагом этого процесса является создание недопустимого SelectionDAG из входных данных. SelectionDAG считается недопустимым, если он содержит инструкции или операнды, которые не могут быть представлены на целевом ЦП. Преобразование из LLVM IR в начальный SelectionDAG является жестко закодированным и законченным фреймворком генератора кода. Рассмотрим пример функции, myDouble, которая принимает целое число в качестве параметра и возвращает ввод, два раза. Реализация кода C для этой функции, myDouble, показана в листинге 4.12, а эквивалентный код LLVM IR показан в листинге 4.13.

Листинг 4.12: Реализация myDouble C

Как описано в разделе 4.1.1, для каждого базового блока кода создается отдельный SelectionDAG. Как обозначено метками (entry, if.then, и if.end)) в листинге 4.13 эта функция содержит три основных блока. Начальный SelectionDAG, построенный для каждого базового блока в IR-коде LLVM для myDouble, показан на рис. 4.2, 4.3 и 4.4. Каждый

Листинг 4.13: IR-код myDouble LLVM

узел графика представляет собой экземпляр класса SDNode. Каждый узел обычно содержит код операции для указания конкретной функции узла. Некоторые узлы хранят значения только в том случае, если другие узлы работают со значениями из узлов соединения. В фигурах SelectionDAG входы в узлы перечисляются в верхней части узла, а выходы выводятся снизу. SelectionDAG может представлять как поток данных, так и управление потоком зависимостей. Рассмотрим SelectionDAG, показанный на рисунке 4.2. Сплошные стрелки (например, соединительный узел t1 и t2) представляют зависимость от потока данных. Однако пунктирные стрелки (например, соединение t0 и t2) представляют зависимость от управляющего потока. Зависимости потока данных сохраняют данные, которые должны быть доступны для непосредственного использования в будущей операции, а также зависимости управления потока сохраняют порядок между узлами, которые имеют побочные эффекты (такие как ветвление/прыжки) [20]. Зависимости управляющего потока называются ребрами цепей (chain) и могут быть видны на фигурах SelectionDAG, поскольку пунктирные стрелки, соединяющиеся с узлом «ch», выводятся на вход их зависимого узла. Иногда требуется настраивать зависимость для конкретных операций. Они могут быть заданы с помощью зависимостей связности, которые могут помочь разделить узлы в планировании. Это видно на рисунке 4.3 стрелкой, соединяющей вывод «связности» узла t3 с входом 2 узла t4. Это необходимо, потому что любые возвращаемые значения

Рисунок 4.2: Начальная myDouble:entry SelectionDAG

Рисунок 4.3: Начальный myDouble: if.then SelectionDAG

Рисунок 4.4: Начальный myDouble: if.end SelectionDAG

не должны быть нарушенными перед возвратом функции. 

4.2.2.2 Оформление 

После того, как SelectionDAG изначально сконструирован, любые инструкции LLVM или типы данных, которые не поддерживаются целевым ЦП, должны быть преобразованы или оформлены так, чтобы целая группа DAG могла быть представлена ​​изначально для определенного назначения. Тем не менее, есть некоторые начальные прогоны оптимизации, которые происходят до оформления. SelectionDAG для базового блока myDouble: entry до оформления, после начальных шагов оптимизации их можно увидеть на рисунке 4.5. Сравнивая это с SelectionDAG до оптимизации (см. Рис 4.2), показано, что узлы t4, t5, t6, t7 и t9 были объединены в узлы t12 и t14. Завершение оформления выполняется сразу после проходов оптимизации. Узаконенный SelectionDAG для базового блока myDouble: entry показан на рисунке 4.6. В качестве примера, чтобы показать, как оформление реализовано, рассмотрите формирование узлов SelectionDAG t12 и t14 (см. Рис. 4.5), в узлы t15, t16 и t17 (см. Рис. 4.6). Реализация оформления инструкций включает в себя как описание TableGen, так и пользовательский код C++ для бэкенда. Пользовательские SDNodes сначала определяются в CJGInstrInfo.td. Два пользовательских определения узлов показаны в листинге 4.14. Несмотря на то, что в заголовочном файле LLVM ISDOpcodes.h имеется множество целевых зависимых операций SelectionDAG, инструкции для этого примера требуют целевых операций: CJGISD::CMP (сравнение) и CJGISD::BR_CC (условное отделение). Эти операции определены в CJGISelLowering.h, как показано в листинге 4.15. Еще одно требование — описать коды условий перехода. Требование кодирует информацию, описанную в таблице 3.5, и показано в листинге 4.16. Реализация оформления описана в CJGISelLowering.cpp, как часть пользовательского класса CJGTargetLowering (унаследованного от класса TargetLowering LLVM).

Рисунок 4.5: Оптимизированный myDouble:entry SelectionDAG

Рисунок 4.6: Оформление myDouble:entry SelectionDAG

Листинг 4.14: Пользовательские определение TableGen SDNode

Листинг 4.15: Определения целевых операций SDNode

Листинг 4.16: Кодирование уловного перехода

Пользовательские операции сначала определяются в конструкторе для CJGTargetLowering, который заставляет метод LowerOperation вызываться, когда эти пользовательские операции встречаются. Нижняя операция отвечает за выбор метода класса для вызова каждой пользовательской операции. В этом примере, методаLowerBR_CC, вызывается. Эта часть реализации оформления показана в листинге 4.17.

Листинг 4.17: Реализация платформенной операции SDNode

Фактическая оформление происходит в методе LowerBR_CC. В строках 11-15 листинга 4.17 показано, как работают значения SDNode (входы узла t14 SelectionDAG показаны на рис. 4.5) хранятся в переменных. Вспомогательный метод EmitCMP (вызывается в строке 19) возвращает SDNode для операции CJG::CMP, а также устанавливает целевую переменную CC в правильный код условия. После того, как эти значения создаются, создаются новые целевые SDNode для этого используется вспомогательный метод getNode, определенный в классе SelectionDAG. Этот узел тогда возвращается через LowerOperation метод и наконец заменяет исходные узлы, t12 и t14, с узлами t15, t16 и t17 (как показано на рис. 4.6).

 

4.2.2.3 Выбор

Фаза выбора является самой крупной фазой в процессе выбора инструкции [20]. Этот этап отвечает за преобразование оформленного SelectionDAG, состоящего из LLVM и пользовательских операций, в DAG, состоящую из целевых операций. Этап отбора в значительной степени зависит от шаблонов, определенных в описаниях инструкций конкурирования (см. раздел 4.2.1.5). Например, рассмотрим шаблоны инструкций АЛУ, показанные в строках 11 и 18 листинга 4.9, а также шаблон условных инструкций перехода, показанный в строке 6 листинга 4.10. Эти шаблоны используются классом SelectionDAGISel для выбора целевых инструкций. myDouble DAG-ах после этапа выбора показаны на рис. 4.7, 4.8 и 4.9. Шаблоны АЛУ совпали с узлами t1 и t3 из myDouble:if.then SelectionDAG которые показаны на рис. 4.3, в узлы t1 и t5, которые видны в DAG, в DAG показаны на рис.4.3. 4.8. Узел t3 myDouble:if.end SelectionDAG показан на рис. 4.4 также соответствовал шаблону АЛУ. Независимая от цели, операция «add“ была заменена целевой операцией ”ADDrr«, которая видна в узле t3 DAG, которая показана на рис.3. 4.9. Пользовательские операции “CJGISD::СМР” и “CJGISD::BR_CC” используются в узлах t16 и t17 из SelectionDAG, которые показаны на рис. 4.6, а также они сопоставимы. Результирующие целевые операции ”CMPri“ и ”JCC» можно увидеть в узлах t16 и t17 DAG, которые показаны на рис. 4.7. После завершения этого этапа все операции SDNode представляют собой целевые инструкции, и DAG готова к планированию.

Рисунок 4.7: Выбранный myDouble: entry SelectionDAG

Рисунок 4.8: Выбранный myDouble:if.then SelectionDAG

Рисунок 4.9: Выбранный myDouble:if.end SelectionDAG

4.2.2.4 Планирование 

Этап планирования отвечает за преобразование DAG инструкций для платформы в список машинных инструкций (представленных экземплярами класса MachineInstr). Планировщик может упорядочить инструкции в зависимости от ограничений, таких как минимизация использования регистров или уменьшение общей задержки исполняемой программы [20]. Как только список машинных инструкций финализирован, DAG завершается. Запланированный список машинных инструкций для функции myDouble приведен в листинге 4.18.

Листинг 4.18: Первоначальный список машинных инструкций для функции myDouble

4.2.3 Распределение регистров 

Этот этап бэкенд части отвечает за удаление всех виртуальных регистров из списка машинных инструкций и их замену физическими регистрами. Для простого RISC механизма, это типичная небольшая процедура необходима для функционального распределения регистров. Основной алгоритм, используемый на этом этапе, называется «жадный распределитель регистров«. Главное преимущество этого алгоритма в том, что он сначала выделяет наибольшие диапазоны необходимых переменных [27]. При наличии динамических переменных, которые не могут быть назначены регистру из-за отсутствия доступных, затем они (переменные) записываются/попадают в память. Затем вместо использования физического регистра инструкции load и store вставляются их в список машинных инструкций до и после момента использования значения. Окончательный список машинных инструкций для функции myDouble приведен в листинге 4.19. Окончательное сопоставление регистров приведено в таблице 4.1. После удаления всех виртуальных регистров код может быть перед ассемблеру. 

Листинг 4.19: Окончательный список команд myDouble

Виртуальный регистр Физический регистр
%vreg0 %R4
%vreg1 %R24
%vreg2 %R24

Таблица 4.1: Карта регистров для функции myDouble

4.2.4 Создание кода 

Заключительного этапа бэкенда, сформировать список машинных инструкций с помощью ассемблера (получить объектный файл для платформы).

4.2.4.1 Ассемблер 

Ассемблер сборки кода требует реализации нескольких пользовательских классов. Класс CJGAsmPrinter представляет подход, который позволяет формировать ассемблерный код. Класс CJGMCAsmInfo определяет некоторую базовую статическую информации для ассемблера, например, определить строку, используемую для комментариев:

CommentString = «//»;

Класс CJGInstPrinter содержит большинство важных функций, используемых ассемблером. Он импортирует код C++, который автоматически создается из AsmWriter TableGenbackend и определяет дополнительные необходимые методы. Одним из таких методов является printMemSrcOperand, который отвечает за печать собственного операнда memsrc, определенного в листинге 4.6. Реализация этого метода показана в листинге 4.20. Метод работает на абстрактном классе MCInst и выводит строковое представление операнда. Окончательный код для ассемблера для функции myDouble приведен в листинге 4.21. Ассемблер сборки добавляет полезные комментарии, а также комментирует метку любого базового блока, который не используется в качестве места перехода в  ассемблерном коде.

Листинг 4.20: Реализация собственного метода printMemSrcOperand

Листинг 4.21: Окончательный ассемблерный код для функции myDouble

4.2.4.2.2 Модуль записи объектов ELF 

Пользовательский машинный код создается в виде объектного файла ELF. Как и в случае с ассемблером, для создания машинного кода необходимо реализовать несколько пользовательских классов.  Класс CJGELFObjectWriter в основном служит оболочкой для своего базового класса, MCELFObjectTargetWriter, который отвечает за правильное форматирование файла ELF. Класс CJGMCCodeEmitter содержит большинство важных функций для выдачи машинного кода. Он импортирует код C++, который автоматически генерируется из эмиттера кода TableGen бэкенда. Этот бэкенд обрабатывает большую часть битового сдвига и форматирования, необходимого для кодирования инструкций, как показано в разделе 4.2.1.4. Класс CJGMCCodeEmitter также отвечает за кодирование пользовательских операндов, таких как операнд memsrc, определенный в листинге 4.6. Реализация метода, ответственного за кодирование этого пользовательского операнда с именем getMemSrcValue, показана в листинге 4.22.

Листинг 4.22: Пользовательская реализация getMemSrcValue

Пользовательский операнд memsrc представляет 21 бит данных: 5 битов требуются для кодирования регистра и еще 16 битов для непосредственного значения. Они хранятся в одном значении, а затем разделяются кодом, автоматически созданным из TableGen. Использование этого пользовательского операнда можно увидеть в листинге 4.23, где показано определение формата инструкций для инструкций загрузки и хранения (как указано в разделе 3.3.1). Строка 7 показывает объявление, строка 11 показывает биты, используемые для значения регистра, и строка 13 показывает биты, используемые для непосредственного значения. Код эмиттера TableGen бэкенда для этого определения создает код C++, как показано в листинге 4.24. Этот код используется при написании машинного кода для инструкции загрузки. В строке 6 показано использование пользовательского метода getMemSrcValue. Строка 7 маскирует всё, кроме регистровых битов, и сдвигает их в правильное место в слове инструкции, а строка 8 делает то же самое, но для 16-битного непосредственного значения.

Листинг 4.23: Определение формата инструкций базовой загрузки и хранения

Листинг 4.24: CodeEmitter TableGen выходной бэкенд для загрузки

Платформо-зависимые машинные инструкции помещаются в “.text» секцию ELF объектного файла. Используется собственный созданный ELF парсер и созданный дизассемблер для архитектуры CJG (см. раздел 5.3), результаты от дизассемблирования ELF файла можно увидеть ниже. Дизассемблированный код и машинный код (в виде файла памяти Verilog) показаны для функции myDouble в листингах 4.25 и 4.26. Это показывает, что ассемблерный код, созданный ассемблером (как показано в листинге 4.21), эквивалентен машинному коду, созданному модулем записи объектов.

Листинг 4.25: Дизассемблированный машинный код для функции myDouble

Листинг 4.26: Машинный код функции myDouble

Отправить ответ

avatar

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.

  Subscribe  
Уведомлять о