Самой простой техникой для симуляции работы некоторого процессора является интерпретация каждой гостевой инструкции, встречающейся в процессе работы модели. Данный подход обладает рядом положительных черт, таких как относительная простота разработки и модификации модели. Однако есть и существенный недостаток — очень низкая скорость работы получаемой модели, зачастую недостаточная для её практического применения. Так, загрузка операционной системы на интерпретирующем симуляторе может занять дни.
Как и в случае с исполнением программ, написанных на языках высокого уровня, имеется следующее решение: вместо того, чтобы на каждом шаге анализировать текст, мы единожды компилируем его в машинный код и затем запускаем полностью подготовленную программу. При этом нет необходимости в перекомпиляции перед каждым запуском.
Если взять набор инструкций целевой машины за входной язык, а инструкции хозяйской машины — за выходной, то можно попытаться «скомпилировать» блоки целевого кода один раз и затем многократно переиспользовать результаты этой работы. При этом исчезает необходимость обращаться к интерпретации инструкций на каждом шаге исполнения.
Подобный процесс получил собственное название двоичная трансляция (ДТ, также бинарная трансляция, БТ, англ. binary translation, BT) [1]. Несмотря на концептуальную схожесть с компиляцией языков высокого уровня, двоичная трансляция имеет существенные особенности, во многом связанные с тем фактом, что исходный для неё язык — машинный код целевой архитектуры — в отличие от языков высокого уровня содержит гораздо меньше информации об алгоритме программы и при этом может быть нагружен различными индивидуальными ограничениями гостевой ЭВМ, затрудняющими эффективную трансляцию и повышающими трудоёмкость написания транслятора.
Преобразование гостевого кода
Общий принцип ДТ состоит в том, что на некотором этапе работы транслятора для блока инструкций, взятых из гостевого приложения и принадлежащих гостевому ISA, в процессе трансляции создаётся новый блок, использующий хозяйские инструкции. Результаты исполнения гостевого кода на гостевой системе и транслированного на хозяйской должны совпадать, т.е. быть семантически эквивалентны. Одновременно могут существовать несколько блоков трансляции, соответствующих разным секциям исходного кода. Каждый их них имеет минимум одну точку входа — адрес, с которого содержащийся в нём код должен начинать исполняться, — и несколько (по крайней мере одну) точек выхода, соответствующих различным ситуациям, при которых симуляция его покидает.
Отдельные блоки трансляции могут быть связаны вместе с помощью т.н. «клея» (англ. glue code), т.е. кода, не соответствующего никакому гостевому, но необходимого для передачи управления между блоками. На рис. 1 показано, как связаны части исходного кода гостевой программы и результат трансляции, состоящий из хозяйских инструкций.
Рис.1: Исходный код базового блока приложения и его связь с результатом двоичной трансляции. Штриховыми линиями показаны точки входа и выхода
Пример преобразования одной инструкции
На рис. 2 приведён пример соответствия гостевой 64-битной инструкции процессора архитектуры Intel® EM64T и блока хозяйского кода, называемого капсулой или сервисной процедурой (англ. service routine), хозяйского процессора, поддерживающего только 32-битные инструкции Intel® IA-32.
Рис.2: Пример соответствия гостевой инструкции и хозяйской капсулы, эмулирующей её семантику и написанной на языке ассемблера. В этом примере хозяйский регистр EBP хранит указатель на структуру гостевого состояния, макросы вида RxX_OFF — смещение внутри гостевого состояния для регистра RxX, а v2h — функция преобразования виртуальных гостевых адресов в хозяйские
Доступ к гостевым регистрам. В рассматриваемом примере используется массив в памяти, различные ячейки которого хранят гостевые регистры. Хозяйский регистр EBP указывает на начало этого массива. По некоторому смещению от его начала, обозначенному RAX_OFF, хранится значение гостевого регистра RAX (строки (6) и (7)), RBX_OFF — смещение для регистра RBX и .т.д. Для того, чтобы выполнить операцию сложения, содержимое памяти загружается в пару 32-битных регистров EDX, EBX (строки (4) и (5)).
Выполнение операции.Поскольку в наборе инструкций IA-32 нет инструкций для операции с 64-битными числами, сложение проводится в два этапа. Сначала складываются младшие 32 бита операндов с помощью инструкции ADDL строка (6). Затем — старшие 32 бита с учётом возможного флага переноса разряда от предыдущего сложения с помощью ADDCL, строка (7).
Чтение гостевой памяти. Ситуация с обращениями к гостевой памяти несколько сложнее. Для её моделирования уже недостаточно просто завести массив в памяти. В общем случае связь гостевых данных и их положения в хозяйском пространстве памяти нелинейна и сложна. В нашем примере это отражено тем, что, перед тем как загрузить первый операнд, вызывается функция v2h, строка (3), единственный аргумент которой сохранён в стеке, строки (1) и (2).
Последняя хозяйская инструкция продвигает симулируемый регистр RIP на длину только что обработанной (3 байта) так, чтобы он указывал на начало следующей инструкции (строка (8)).
Размер капсулы
Для «идеального» ДТ для некоторой пары архитектур желательно выдерживать соответствие «одна хозяйская инструкция эмулирует одну гостевую» для каждой капсулы. Из-за неполного соответствия окружений гостя и симулятора это почти никогда не выполняется, возможны следующие ситуации.
1. На одну гостевую приходится несколько хозяйских инструкций, в сумме компенсирующих различия между архитектурами.
2. На одну гостевую приходится ноль хозяйских инструкций. Такая ситуация возникает, если исходная команда не изменяет архитектурного состояния и может быть опущена в функциональной модели. Примеры: операции предвыборки в кэш, подсказки для предсказателя переходов.
3. Соединяющий блоки трансляции клей не соответствует ни одной гостевой инструкции и необходим только для работы симулятора.
Особенности реализации ДТ
Разумно ожидать, что чем больше похожи целевая и хозяйская архитектура, тем проще создавать ДТ и тем быстрее он должен работать. Для особого случая, когда эти архитектуры совпадают, может оказаться, что никакого преобразования производить и не требуется — целевой код уже «готов» для исполнения. Верно и обратное — чем сильнее различаются архитектуры гостя и хозяина, тем больше усилий приходится вкладывать в реализацию ДТ и симулятора в целом.
Семантика инструкций
Всё множество команд современных процессоров можно разделить на несколько классов согласно выполняемой ими функции. Расcмотрим особенности, характерные для симуляции инструкций каждого из них.
Арифметические целочисленные.Практически все существующие ISA имеют команды для арифметических, логических и сдвиговых операций над целыми числами, и их эффективное моделирование в составе ДТ, как правило, вызывает минимальные проблемы.
Инструкции с числами с плавающей запятой.Поддержка разными процессорами существенно различается, несмотря на наличие стандарта IEEE 754 [6], призванного внести унификацию. Некоторые архитектуры могут оперировать числами только одинарной (32 бита) или двойной (64 бита) точности. Другие используют нестандартные форматы, например, сопроцессор x87 IA-32 использует внутреннее представление чисел шириной 80 бит, а в IA-64 машинный формат имеет 82 бита. Машинная поддержка половинной (16 бит) и четырёхкратной (128 бит) точности, а также форматов с основанием десять присутствует в ограниченном числе систем. Кроме представления чисел, сами арифметические операции могут быть реализованы по-разному. Они различаются доступными режимами округления результатов, способами индикации ошибочных ситуаций, поведением для т.н. денормализованных (англ. denormalized) чисел и т.д. Интересующийся читатель найдёт подробное описание в [4]. Библиотека SoftFloat [5] реализует стандарт IEEE 754 с помощью только целочисленной арифметики, тем самым предоставляя переносимую реализацию.
Векторные инструкции.Используются для параллельного выполнения операции над векторами значений, хранящихся в специальных регистрах шириной до 512 бит. Примеры: Intel® SSE, AVX, AVX2, IBM* AltiVec [8]. При симуляции в случае, если хозяин не имеет аналогичной инструкции, она может быть представлена с помощью последовательного выполнения операции над всеми элементами вектора. Таким образом, векторные операции сводятся к своим последовательным вариантам.
Контроль управления.В этот класс включаются инструкции, изменяющие значение указателя текущей команды PC, т.е. условные и безусловные переходы, вызовы процедур и возвращения из них, программного прерывания и т.д. В разных архитектурах они отличаются очень сильно. Поэтому чаще всего их капсулы получаются достаточно длинными. Общая задача симулятора при их обработке — вычисление точки входа в новый блок трансляции, соответствующий гостевому адресу перехода. При этом приходится учитывать возможность ситуации, в которой она отсутствует или некорректна.
Привилегированные инструкции.В большинстве архитектур некоторая часть команд может исполняться, только если процессор находится в специальном режиме, иначе они вызывают исключение. В этом режиме работает операционная система, имеющая неограниченный доступ ко всем ресурсам системы. Привилегированные инструкции специфичны для каждой системы и обычно семантически нагружены, поэтому их симуляция требует длинных капсул.
Ситуация не улучшается даже при полном совпадение архитектур гостя и хозяина. Так как исполнение привилегированных команд в непривилегированном режиме, в котором обычно работает сама программа-симулятор, невозможно, их приходится заменять последовательностью разрешённых инструкций.
Прочие.Существует достаточно много инструкций различных ISA, не подпадающих под данную выше классификацию или имеющих специфику, требующую особого внимания при симуляции. Это могут быть строковые, предикатные, длинные инструкций, слоты задержки у переходов и т.п.
Сходства и различия в архитектурных состояниях
Хранение состояния целевой системы в выделенном буфере памяти обладает недостатком — необходимостью часто обращаться к медленному ОЗУ и испытывать большие задержки при промахах кэша. Поэтому создатели систем ДТ стараются разместить максимально возможное число целевых регистров на хозяйских, чтобы при обращении к ним требовалось минимальное время. Это легко осуществить, если в архитектуре хозяина предусмотрено большее число регистров, чем необходимо гостю. Например, это верно для комбинации гостевой системы IA-32 c 8 регистрами общего назначения и хозяина архитектуры MIPS с 31 регистром. При этом максимальная ширина доступных регистров также может различаться. Например, для модели 64-битной архитектуры IA-64 не получится уместить гостевой регистр целиком в хозяйском, если хозяйская система — 32-битная, например, ARMv7.
Если эти условия на регистровый файл не выполняются, то приходится прибегать к различным ухищрениям. Так, только часть регистров может быть отдана под нужды симуляции, а один гостевой регистр приходится «разбивать» на несколько частей, по отдельности умещающихся в хозяйских.
Особенности обработки доступов к памяти и устройствам
Несмотря на то, что операции чтения и записи памяти присутствуют почти во всех архитектурах процессоров, за историю развития вычислительной техники было придумано неисчислимое количество способов адресации и обращения к ней. Не пытаясь объять необъятное, приведём лишь несколько примеров.
- В архитектуре IA-32 адрес операнда в памяти может определяться несколькими регистрами и константами, закодированными в инструкции. В самом общем случае в ней определяется сегмент, база, индекс и масштабный коэффициент, а также одна константа, определяющая смещение и поле, изменяющее ширину. Для контраста: в системах с процессорами MIPS в адресация используется один регистр и одна константа.
- В ряде случаев ячейка памяти может адресоваться нулём операндов, т.е. неявно, например, располагаться на вершине стека.
- Поддерживаемые размеры доступов в память могут быть различными. Например, хозяин за одну операцию может прочитать максимум 32 бита, тогда как в гостевой архитектуре требуемый размер считываемых данных равен 64 битам. Это усложняет моделирование атомарных операций, т.к. приходится разбивать гостевой доступ на несколько транзакций, нарушая исходные предположения о неделимости последнего. Обратная ситуация, когда, например, требуется прочитать 1 байт гостевой памяти, но хозяин может адресовать только 4 байта, тоже может привести к ошибкам вы симуляции.
- Отдельно следует отметить различия в требованиях разных систем к выравниванию (англ. alignment) доступов в память (Блок памяти длиной w является выровненным по адресу A , если A = 0 mod w, т.е. A нацело делится на w. При этом чаще всего рассматривается выравнивание по степеням двойки). Некоторые архитектуры запрещают невыровненные доступы — при попытке прочитать или записать данные по такому адресу возникает исключение, тогда как другие процессоры это позволяют, зачастую облагая такой доступ повышенным временным «пенальти».