Quantcast
Channel: Блоги Intel® Developer Zone
Viewing all articles
Browse latest Browse all 106

Двоичная трансляция для задач симуляции. Часть 2. Статика и динамика

$
0
0

Попытаемся ответить на два следующих вопроса. 
1. Какой должна быть единица ДТ? Другими словами, чем определяется количество и расположение целевых инструкций, обрабатываемых за один проход транслятора?
2. Как должны быть связаны во времени фазы трансляции и симуляции? Должна ли одна из них предшествовать второй, или они должны чередоваться?

Для обычных языков высокого уровня ответ на первый вопрос почти очевиден — исходный файл с текстом программы (или модуля) компилируется в приложение (объектный файл), самодостаточное в плане дальнейшего исполнения или использования. Более мелкие единицы компиляции, такие как процедуры, также имеет смысл транслировать целиком, так как при их использовании понадобится весь их код.
В случае ДТ возникают сложности из-за того, что входной текст таких систем — «монолитный» машинный код, не имеющий меток начала отдельных субъединиц, зачастую с перемешанными секциями кода и данными, неопределёнными адресами переходов и т.п.

Статическая ДТ.
Хотя аналогичная компиляции техника трансляции гостевого приложения целиком в образ хозяйского кода (статическая ДТ) иногда применялась [2], она не получила широкого распространения по ряду причин.
Будучи применимым для трансляции отдельных пользовательских приложений, статическая ДТ становится невозможной в случае полноплатформенной симуляции, при которой пришлось бы транслировать всю память гостевой ЭВМ. Во-первых, объём входного текста может быть огромен, и время трансляции, и размер результирующего файла окажутся непозволительно большими. Во-вторых, содержимое памяти, в том числе секций с кодом, изменяется в ходе работы, что делает статическую ДТ бессмысленной — результирующий код в силу своей неизменности не будет отражать правильное состояние изменяемой памяти.
С другой стороны, будучи однажды полученным и сохранённым в файле на диске, результат статического преобразования приложения может запускаться неограниченное число раз, что компенсирует время, потраченное на его получение. Поэтому на этапе ДТ могут быть применены разнообразные оптимизации, нацеленные на создание максимально эффективного кода.

Динамическая ДТ.
Для задач симуляции более адекватным является иной подход, в котором моделирование гостевой системы (то есть исполнение оттранслированного кода) перемежается с запусками механизма двоичной трансляции для новых блоков кода, которые будут вскоре исполнены, а также с обновлениями трансляций для блоков, изменивших своё содержимое. При этом в памяти симулятора хранятся ранее оттранслированные секции для их переиспользования в случае, если управление вновь перейдёт на них (рис. 3).


Рис.3: Динамическая ДТ. Фаза симуляции, использующая сгенерированный код, периодически сменяется фазой трансляции, хранящей уже существующие и создающей новые секции хозяйского кода из гостевого

Отметим, что, в отличие от статической, при динамической ДТ время, потраченное на фазу преобразования, фактически отнимается у фазы симуляции, т.е. негативно сказывается на производительности модели. Поэтому спектр возможных оптимизаций более ограничен, использованы могут быть только достаточно быстрые из них.


Обнаружение кода.
Следующие обстоятельства необходимо учитывать в процессе трансляции блоков инструкций.

  • В оперативной памяти данные программ (переменные, массивы) и код (инструкции), их обрабатывающий, хранятся вместе. В общем случае никаких границ между ними не обозначено. Трансляция секций данных бесполезна: управление никогда не будет передано на них, — и даже вредна: затрачиваемое время уходит впустую. Необходим критерий, определяющий целесообразность выполнения ДТ для некоторого региона памяти.
  • В архитектурах, допускающих переменную длину инструкций, очень важен адрес, с которого начинается их декодирование, интерпретация или трансляция. Сдвиг даже на один байт приводит к изменению смысла до неузнаваемости (рис. 4). Кроме того, результат декодирования может зависеть от режима процессора, и если в ходе симуляции он изменился (например, процессор перешёл из 32-битного в 64-битный режим), то предыдущие блоки трансляции, скорее всего, перестали соответствовать исходному коду.




Рис.4: Обнаружение кода. Смысл содержимого памяти меняется при изменении стартового адреса. Пример двух интерпретаций для фрагмента кода архитектуры IA-32


Указанные проблемы определяют задачу обнаружения кода (англ. code discovery). Точное её решение зависит от особенностей архитектур гостя и хозяина. Отметим лишь два ключевых момента.

  1. Некоторый регион в памяти разумно подвергать ДТ, если вероятность того, что в некоторый он будет исполнен, хотя бы ненулевая. Это верно в случае, когда он достижим из других, уже оттранслированных частей программ, т.е. известно, что некоторые инструкции передачи управления указывают на него. Если код исполнялся раньше, также велика вероятность того, что он исполнится в будущем. 
  2. Очень важно кроме собственно содержимого блока трансляции хранить и все допустимые точки входа в него, т.е. адреса, попадающие на границы инструкций, а также ассоциировать режим процессора, для которого блок был создан.


Отметим, что задача обнаружения кода при ДТ во многом связана с поддержкой ситуации самомодифицирующегося кода, которая будет описана далее.

Единицы трансляции.
Память хозяйской системы ограничена, что возвращает нас к первому вопросу — как выделять и организовывать блоки трансляций, чтобы получить приемлемую скорость симуляции, при этом не исчерпав ёмкость хозяйского ОЗУ? Кроме того, необходимо определиться, какие блоки хранить, а какие выбрасывать, какую длину в байтах они должны иметь. Рассмотрим два возможных решения этих задач, которые основываются на принципе локальности исполнения и ограниченности рабочего набора [9].


1. Трасса исполнения — это запись истории того, в каком порядке инструкции когда-то были исполнены. Как правило, трасса имеет ровно одну точку входа, соответствующую первой её инструкции. Из общих свойств алгоритмов следует высокая вероятность того, что впоследствии эти инструкции будут исполнены снова в том же порядке. При этом если они формируют базовый блок (т.е. среди них не встречается команд условного или непрямого перехода), то порядок их исполнения будет в точности такой же, как и в первый раз. Следует отметить, что первоначальное создание трасс, когда никакой истории исполнения ещё нет, приходится организовывать с помощью альтернативного механизма симуляции, например, интерпретацией (рис. 5). Прерывать создание трассы нужно по ряду условий в гостевом коде, после которых направление исполнения неизвестно или существенно отличается, например, на исключениях, прерываниях, командах смены режима процессора и т.п.

 

Рис.5: Двоичная трансляция целых страниц. Для ранее исполненных блоков переиспользуются оттранслированные секции хозяйского кода. Процесс симуляции прерывается для трансляции новой страницы

2. Инструкции, располагающиеся в памяти по соседним адресам, скорее всего, относятся к связанным частям алгоритма программы, будут выполняться вместе и поэтому могут быть оттранслированы в один блок (рис. 6). В этом случае единицей трансляции является гостевая страница фиксированного размера. В отличие от трасс, страница трансляции может иметь множество точек входа — каждый адрес, соответствующий началу гостевой инструкции на ней, может быть использован таким образом. Однако необходимо следить, чтобы управление не передавалось «в середину» инструкции — в таком случае трансляция некорректна.

 

Рис.6: ДТ с трассами исполнения. Первое исполнение каждой гостевой инструкции производится с помощью интерпретатора, при этом также осуществляется её трансляция и сохранение результата в трассе 

Кроме того, трансляция кода на текущей странице может быть прервана по достижении блока хозяйского кода определённого объёма. Как и в случае с трассами, разумно прерывать процесс ДТ при обнаружении инструкции условного или непрямого перехода.
Хорошее описание приёмов ДТ, в ряде источников называемой JIT-компиляцией (англ. just in time), дано в [10].

Проблема самомодифицирующегося кода
Большая доля современных архитектур процессоров для ЭВМ построена согласно принципам фон Неймана. Один из них состоит в том, что исполняемый код и обрабатываемые им данные располагаются в одной физической памяти. Следствие этого — возможность создания программ, которые в процессе работы изменяют код других программ и, в частности, свой собственный. Затем этот новый код может быть исполнен. Мы будем обобщённо обозначать такое явление, как самомодифицирующийся код (англ. self-modifying code, SMC). Для программ с SMC не все инструкции приложения известны до момента их генерации во время работы уже запущенного приложения.

Это обстоятельство фактически делает системы статической ДТ, не имеющие слой симуляции времени выполнения, функционально несостоятельными — они не могут корректно транслировать такой код. 

Симулятор, задействующий динамическую двоичную трансляцию, сам по себе является программой с самомодифицирующимся кодом, так как на фазе симуляции управление передаётся на код, отсутствующий в исходном файле приложения, — он был создан «на лету» на фазе трансляции. 

При исполнении самомодифицирующейся программы гостевой код изменяется, и есть вероятность, что уже существующие блоки транслированного кода, соответствовавшие первоначальному состоянию памяти, перестанут подходить новому содержимому, и при передаче на них исполнения результат вычислений будет некорректен. Для предупреждения этого необходимо отслеживать все записи в память и сбрасывать или ретранслировать затронутые при этом блоки.
Поскольку процесс ДТ одного блока занимает существенное время, скорость работы симулятора для участков программ с самомодифицирующимся кодом может резко падать — блоки живут недолго, часто отбрасываются как устаревшие, исполнение часто прерывается на ретрансляцию. В таких случаях простой интерпретатор может показывать более высокую скорость симуляции.
Следует иметь в виду, что детали поведения процессора при SMC могут отличаться на разных архитектурах. Обусловлено это тем, что в реальности инструкции также берутся не непосредственно из памяти, а из более быстрых буферов, куда они были помещены специальными механизмами предварительной загрузки, и состояние памяти может не соответствовать их содержимому. Так, для систем с раздельными кэшами инструкций и данных (ARM, MIPS) результат модификации кода проявится только после выполнения специальных инструкций сброса кэшей. В архитектуре Intel IA-32 гарантируется, что результат SMC будет виден для исполняющего устройства немедленно. Исключением является изменение инструкции непосредственно под указателем инструкций — оно не будет видно программе, пока текущая инструкция не закончится. 
В любом случае обеспечение работы SMC требует сброса части состояния и повторного считывания его из памяти, что вносит некоторую задержку в исполнение, и при неправильной организации кода его производительность может сильно пострадать.
Ситуация усложняется, когда в моделируемой системе есть несколько агентов, способных модифицировать память, например, в многопроцессорных системах или в платформах, где устройства могут писать в память напрямую (англ. direct memory access, DMA). В таких случаях модель должна отслеживать все такие доступы и отбрасывать устаревшие блоки.


Viewing all articles
Browse latest Browse all 106

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>