На правах рекламы:
ISSN 0236-235X (P)
ISSN 2311-2735 (E)

Авторитетность издания

ВАК - К1
RSCI, ядро РИНЦ

Добавить в закладки

Следующий номер на сайте

2
Ожидается:
16 Июня 2024

Технология двоичной совместимости программно-аппаратных средств

Статья опубликована в выпуске журнала № 1 за 1999 год.
Аннотация:
Abstract:
Автор: Рожков С.А. () -
Количество просмотров: 13198
Версия для печати
Выпуск в формате PDF (1.25Мб)

Размер шрифта:       Шрифт:

Проблема сохранения битовой совместимости, то есть возможности выполнять существующее программное обеспечение на вновь разрабатываемых микропроцессорах, всегда была и остается одной из важнейших для успеха на компьютерном рынке. С одной стороны, требуется непрерывно повышать производительность микропроцессоров (не забывая, конечно, и об их стоимости), с другой – принятые в более ранних разработках архитектурные решения сильно сковывают введение новых идей для повышения все той же производительности.

Появление в восьмидесятых годах RISC-процессоров, казалось, окончательно поставило точку на развитии CISC'ов. Значительное преимущество в скорости и относительно небольшой объем математики для старых платформ сделали возможным новым архитектурным платформам на основе RISC (MIPS, SPARC, PA-RISC, PowerPC, а позднее DEC ALPHA) приобрести своих пользователей. Долгое время CISC'и (а это прежде всего, конечно, Intel и его клоны AMD, Cyrix, NEC и др.) и не рассматривались как конкуренты RISC'ов (по крайней мере, в области рабочих станций и серверов). Тем временем шло значительное накопление математического обеспечения для Intel-совместимых процессоров, а суперскалярный подход (одновременная аппаратная выборка и исполнение нескольких команд за такт), применявшийся в RISC-процессорах, стал использоваться и в процессорах фирмы Intel. Появление таких процессоров, как Pentium и Pentium Pro, для некоторых из перечисленных фирм оказалось полнейшей неожиданностью. Рынок рабочих станций, на котором безраздельно господствовали RISC'и, был сильно потеснен Intel'ом. Обладая огромнейшим количеством программных продуктов и пользователей, Intel продемонстрировал производительность даже в ряде случаев выше, чем RISC'и. И уже теперь фирмы, производящие процессоры для рабочих станций, вынуждены были искать пути совместимости с Intel х86 [1,2] сначала путем программной интерпретации, а затем с использованием метода двоичной компиляции кодов х86 в коды целевой платформы.

С другой стороны, фирмам приходится решать проблему совместимости и с собственными ранее выбранными платформами. Так, переход фирмы Digital на архитектуру ALPHA AXP значительно более быструю, но абсолютно двоично несовместимую с VAX и MIPS, поставил перед фирмой задачу переноса математического обеспечения с этих платформ на ALPHA. Это было довольно успешно решено при помощи двоичной компиляции пользовательских приложений с VAX и MIPS на ALPHA [3], что позволило фирме сохранить основных своих пользователей и даже попытаться привлечь через соответствующие двоичные компиляторы пользователей других фирм (SUN Microsystems [4] и Intel [5]) на свою новую платформу.

В то же время последнее десятилетие отмечено массовым переходом разработчиков микропроцессоров на параллельную обработку команд. Разработанные новые алгоритмы оптимального планирования и распределения аппаратных ресурсов, реструктуризация управления в программах позволили в несколько раз поднять производительность компилируемых программ по сравнению с классическими методами оптимизации. Однако применение этих алгоритмов в существующих RISC-процессорах существенно затруднено из-за необходимости строго выдерживать совместимость, с одной стороны, а с другой – из-за невозможности учета динамической составляющей планирования в суперскалярных реализациях. Это привело к развитию нового направления в микропроцессорах, а именно: архитектуры с явно выраженным параллелизмом исполняемых команд, или архитектуры широкого командного слова, получившей название VLIW (Very Long Instruction Word) [6,7]. Значительное сокращение аппаратуры в этой архитектуре за счет распределения ресурсов и решения проблемы информационной зависимости компилятором позволило увеличить тактовую частоту и число исполнительных устройств. Но обратной стороной этого стала полная несовместимость VLIW'а с другими архитектурными платформами. Долгое время это сильно сдерживало фирмы от реализации универсального VLIW-процессора. Создавались лишь специализированные микропроцессоры с широким командным словом для специализированной обработки данных, где не требовалась совместимость (например мультимедийный процессор TriMedia фирмы Philips Electronics).

Но в 1994 году Intel и Hewlett Packard объявили о начале совместной разработки нового универсального процессора Merced, основанного на архитектуре широкого командного слова (справедливости ради надо отметить, что к этому моменту уже был создан универсальный российский компьютер "Эльбрус-3" [7,15] с архитектурой широкого командного слова). В дальнейшем эта архитектура получила название EPIC (Explicitely Parallel Instruction Computing) [8]. Помимо перехода на явный параллелизм исполняемых команд, в этой архитектуре используется расширенный 64-разрядный адрес, что ознаменовало переход Intel'а от IA-32 к IA-64. Чтобы сохранить своих пользователей, обе фирмы сразу же объявили, что Merced будет способен выполнять коды как архитектуры х86, так и PA-RISC. Но пути решения этой задачи оказались существенно различными. Если коды х86 аппаратно перекодируются в коды Merced, то коды PA-RISC будут переноситься на новый процессор при помощи двоичной компиляции. При этом аппаратная перекодировка, кроме увеличения объема оборудования, приведет к заметному снижению производительности в режиме IA-32 по сравнению с "родными" процессорами Pentium-II фирмы Intel.

Таким образом, двоичная компиляция в настоящее время становится реальной помощью в решении проблемы совместимости как при разработке собственных архитектур крупными компьютерными фирмами, так и (тем более!) при стремлении ввести новую архитектуру небольшими компьютерными компаниями, для которых единственный путь успеха (по крайней мере, на начальном этапе) – быть совместимыми с известной архитектурной платформой. При этом использование двоичной компиляции имеет ряд несомненных преимуществ по сравнению с аппаратными декодерами:

¨   более легкое исправление несовместимых ошибок;

¨   более агрессивные оптимизации;

¨   сохранение оттранслированного кода;

¨   меньшая зависимость от конкретного целевого процессора;

¨   меньшая стоимость.

Двоичная компиляция

Задачи переноса математического обеспечения через перетрансляцию низкоуровневых программ на ассемблере в ассемблер целевой платформы применялись еще в эпоху мини-компьютеров [9,10]. Для этого использовались декомпиляторы, то есть программы, восстанавливающие из ассемблерного текста исходной платформы (или даже из объектного кода) [11] текст на языке высокого уровня с последующей трансляцией на целевой машине. При этом вполне естественно накладывались определенные ограничения на множество воспринимаемых кодов и использовались известные программные соглашения для исходной машины. Тем не менее даже с ограничениями удавалось переносить части операционной системы [10]. Идеи двоичной трансляции применялись также для анализа и модификации программ в кодах [12].

Вообще говоря, можно рассматривать двоичную компиляцию как транслирующий интерпретатор. Существуют весьма неплохие интерпретаторы, в том числе и для Intel х86 [13]. Обладая высокой степенью надежности (под надежностью здесь и далее понимается не только надежность соответствующего программного продукта в смысле устойчивости работы программы, но и полнота реализации интерпретируемой платформы), интерпретаторы в то же время значительно проигрывают по скорости исходной машине (в 20 и более раз) [1].

Для того, чтобы как-то улучшить эту ситуацию, разработчики интерпретаторов нашли компромиссное решение – исполнять только пользовательские приложения для популярных операционных систем. Естественно, для Intel х86 – это OS Windows (3.1, 9х, NT). Известно, что программы, написанные для Windows, просто пронизаны вызовами функций, реализующих Windows API (60- 80 %) [2]. Поэтому библиотеки, осушествляющие этот программный интерфейс, реализуются на целевой платформе средствами той операционной системы и графическими библиотеками, которые установлены на целевой машине. Остальная часть приложения х86 просто интерпретируется (например системы Wabi фирмы SUN Microsystems и SoftWindows фирмы Insignia Solutions). Такой подход позволил существенно поднять быстродействие исполнения приложений. Однако и он несвободен от недостатков. Во-первых, исполняются только пользовательские приложения для одного операционного окружения. Во-вторых, полнота реализации программного интерфейса библиотек Windows оставляет желать лучшего, поскольку существует масса недокументированных функций, которые приложения, тем не менее, свободно используют. А отсюда страдает надежность интерпретирующей системы. И, наконец, в-третьих, скорость исполнения недостаточно высока.

С целью повышения производительности подобных систем в них стали применяться методы двоичной трансляции. Фрагменты кода х86 (как правило, это линейные участки) транслируются во время исполнения и запоминаются в программном кэше. Таким образом, при повторном переходе на такой фрагмент система пытается отыскать его в кэше, и, если он там есть, управление передается на уже оттранслированный код, то есть исключается разбор исходного кода и его интерпретация [2]. При этом в некоторых системах использовались методы локальной оптимизации на линейном участке.

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

Шаг 1. Устранение излишней виртуальности по памяти. Как правило, интерпретатор эмулирует память исходной машины. Дополнительная косвенность, естественно, привносит потери на обращения в память. Если располагать исходную программу и ее данные по тем же адресам, что и в исходной машине, то тем самым мы упростим реализацию команд обращения в память, но возникнет задача сокрытия интерпретирующей системы. Этого можно достигнуть за счет выделения альтернативного пространства в памяти, использования расширенного адреса или закрытия той области памяти, в которой находится интерпретатор, по обращению (как по чтению/записи, так и по исполнению), с тем чтобы интерпретируемая программа не смогла обнаружить интерпретатор в "своем" адресном пространстве. Код исходной программы требуется сохранять в памяти на случай обращения в него за константами.

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

Шаг 3. Сохранение кода. Следующим естественным шагом является сохранение кода для каждой оттранслированной команды в программном кэше или на внешнем носителе. Это позволит избежать повторной обработки команд исходной программы. Однако для обнаружения случаев модификации кода требуется закрывать код исходной программы по записи. Таким образом, при попытке записать что-то в область кода произойдет прерывание, которое обработает система динамической поддержки двоичного компилятора (например повторная перетрансляция модифицированного участка кода).

Шаг 4. Оптимизации. На этом шаге можно рассмотреть три аспекта: удаление избыточного кода, отложенные вычисления и перестановка обращений в память. Первая оптимизация связана со сбором общих подвыражений, с лишними пересылками на границах команд; вторая имеет отношение к частично вычислимой семантике. Например, практически каждая команда х86 имеет в качестве своего побочного эффекта модификацию регистра флагов. Но вычислять флаги при реализации семантики каждой команды х86 в коде целевой машины слишком расточительно, поскольку реально эти флаги используются перед командой перехода и, возможно, при обработке прерывания. Для последнего случая достаточно удерживать исходные операнды до момента их переиспользования, с тем чтобы в случае прерывания можно было бы пересчитать модификацию флагов. До сих пор все шаги не приводили к нарушению надежности исполнения двоичного кода, поскольку каждое улучшение (в смысле производительности) сопровождалось необходимой поддержкой, и порядок обращений в память был неизменен. Однако немалую роль в ускорении работы двоично оттранслированной программы (особенно для архитектур с явной параллельностью исполняемых команд) играют перестановки обращений в память. Используя технику планирования операций и специальную аппаратную поддержку удается решить и эту проблему. Применение оптимизаций в полном объеме позволяет поднять эффективность исполнения двоично оттранслированного приложения до 70 % (и даже выше) по сравнению с тем же самым приложением, оттранслированным с текста в коды целевой платформы [5].

Двоично компилирующие системы можно разделить на два класса [3]: закрытые системы и открытые. В первых весь код исходной программы должен быть представлен до момента начала трансляции. Понятно, что без вмешательства пользователя оттранслировать полностью исходную программу в большинстве случаев не удастся, поскольку нужно представить все используемые библиотеки, указать все случаи динамически создаваемого кода и самомодификацию кода, задать все точки входа. Открытые системы подразумевают, что код может меняться и создаваться во время исполнения. Такие системы рассчитаны на автоматическое восстановление и продолжение исполнения в сбойных ситуациях и не требуют вмешательства пользователя. Эти задачи возложены на динамическую поддержку исполнения двоично откомпилированного кода.

Открытые системы тоже можно разделить на две части: динамические двоичные компиляторы и статические компиляторы. В первых весь анализ исходного кода и генерация целевого кода производятся во время исполнения, используя, как правило, программный кэш для сохранения полученного кода. При переполнении такого кэша фрагменты двоичного кода вычеркиваются и заново перекомпилируются при повторном исполнении. На внешнем носителе двоично откомпилированный код не сохраняется [14]. Но эффективность полученного кода невысока. Для проведения необходимых оптимизаций просто не хватает времени, в противном случае сильно замедлится скорость исполнения приложения. Статические компиляторы предварительно анализируют код исходной машины, пытаясь на этапе разбора выявить как можно больше точек входа и отделить код от данных, если это не удается, задача получения дополнительной информации возлагается на динамическую поддержку и последующую (скрытую) перекомпиляцию.

Несмотря на успехи ряда проектов двоично компилирующих систем, все они несвободны от недостатков:

-         неполная совместимость с исходной эмулируемой архитектурой (двоичная компиляция реализуется для уже разработанных несовместимых платформ);

-         недостаточно эффективный результирующий код (применяются методы ограниченной оптимизации полученного кода);

-         переносятся, как правило, только пользовательские приложения (привилегированная часть либо эмулируется на целевой архитектуре, либо перенос осуществляется в рамках одной операционной системы);

-         двоичная компиляция полагается на программные соглашения (правила использования регистров эмулируемой архитектуры, правила вызова процедур и т.д.).

Исходя из этого при разработке микропроцессора "Эльбрус" на основе широкой команды было принято решение использовать двоичную компиляцию для реализации режима совместимости с известными архитектурными платформами (в первую очередь Intel x86, SPARC), поскольку практически невозможно внедрить новую несовместимую архитектуру на рынке универсальных микропроцессоров, не имея возможности исполнять старые накопленные коды. Но отсюда следует, что требования к двоичному компилятору "Эльбрус" существенно более жесткие, чем к существующим двоичным компиляторам, а именно:

·     полная совместимость с выбранной (или выбранными) базовыми платформами (100 % надежности);

·     максимально возможная эффективность двоично откомпилированного кода, но по меньшей мере не хуже соответствующих базовых платформ в момент выхода на рынок "Эльбруса";

·     "прозрачность" двоичного компилятора "Эльбрус" (скрытие его от пользователя).

Чтобы избежать упомянутых недостатков, присущих существующим на рынке двоичным компиляторам, в основу двоичного компилятора "Эльбрус" положены следующие принципы:

*         совпадение основных арифметико-логических операций (это позволяет избежать программной интерпретации базовых операций и, следовательно, повысить эффективность);

*         аппаратная поддержка некоторых особенностей базовой архитектуры (формирование условных флагов, нестандартная плавающая арифметика, особые операции типа префикса LOCK в Intel x86, обращение в память и т.д.);

*         сохранение состояния памяти адекватного базовой машине (программа и данные располагаются по тем же адресам);

*         выдерживается порядок записей в память (вообще говоря, требуется полностью сохранить порядок обращений в память, но с точки зрения обеспечения соответствия модели памяти и эффективности, при условии, что адреса чтения и записи не конфликтуют, можно переставлять чтения из памяти и чтения и записи между собой);

*         аппаратная поддержка исполнения двоично оттранслированного кода (обнаружение всякого рода несоответствий принятых решений при оптимизации кода реальному поведению программы, например при вводе/выводе);

*         использование алгоритмов оптимизации компилятора "Эльбрус" [16] для получения высокоэффективного двоичного кода.

В качестве базового подхода выбран принцип достаточно простой статической трансляции с динамической поддержкой во время исполнения (на основе динамического компилятора). Аппаратная и системная поддержка процесса исполнения двоичного кода гарантирует достижения 100 % надежности, а архитектура широкого командного слова и оптимизирующий компилятор обеспечивают высокую скорость исполнения.

Структура двоичного компилятора

Рассмотрим технологию двоичной компиляции для простоты изложения на примере реализации режима совместимости одного из вариантов микропроцессора "Эльбрус" с платформой SPARC [17] для операционной системы Solaris.

Любой компилятор состоит из двух основных частей: модуля грамматического разбора (front-end) для какого-либо входного языка и модуля, включающего оптимизатор и кодогенератор (back-end). Входной модуль двоичного компилятора, как и любой другой, состоит из сканера, синтаксического и семантического анализаторов, а в качестве промежуточного представления генерируется управляющий граф, который затем семантическими процедурами переводится в промежуточное представление транслятора "Эльбрус" (подстановка семантики).

Процесс двоичной компиляции состоит из четырех основных шагов:

-         статический анализ;

-         синтез результирующего кода (включая оптимизации);

-         динамический анализ при исполнении программы;

-         перекомпиляции (если необходимо).

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

·     выявление вызовов формальных процедур и возвратов из процедур;

·     идентификация операторов "switch";

·     идентификация локальных переменных (для дальнейших оптимизаций).

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

Задачи на стадии синтеза:

*         архитектурное отображение SPARC на "Эльбрус" (соответствие регистров, программная эмуляция ряда особенностей SPARC-архитектуры и т.д.);

*         трансляция операций SPARC во внутреннее представление компилятора "Эльбрус";

*         замена некоторых вызовов SPARC-подпрограмм на операции "Эльбрус";

*         специальный режим оптимизаций.

Стадия динамического анализа необходима, в частности, если косвенный переход с неизвестным при анализе адресом (адрес SPARC) встречается на этапе исполнения. Весь исходный SPARC-код, загруженный в память "Эльбрус", закрыт на исполнение. В этой ситуации происходит прерывание, и операционная система передает управление динамическому анализу, который сбрасывает новую информацию в дополнительный файл (используется на этапе статического анализа при перекомпиляции SPARC-кода) и выполняет процесс восстановления. Для этого производится переключение в режим динамического двоичного компилятора (вариантами могут быть режим прямого исполнения в аппаратуре или вызов интерпретатора SPARC-кода), после достижения подходящей ситуации (конец процедуры, переход на известную метку) производится переключение на код "Эльбрус". По окончании исполнения программы автоматически вызывается двоичный компилятор, который с учетом новой информации перекомпилирует исходный SPARC-код.

Конечно, любая SPARC-программа с определенным набором входных данных отображается в определенную программу "Эльбрус". Однако чем больше набор входных данных, тем более полную результирующую программу в коде "Эльбрус" мы будем иметь. В ряде случаев статический анализ может выявить весь SPARC-код без динамического анализа.

В процессе оптимизации, помимо обычного множества преобразований, применяются специальные оптимизации: перенос локальных переменных из SPARC-стека и параметров в регистры "Эльбрус", переупорядочивание операций чтения/записи из памяти с использованием аппаратной поддержки и программных методов восстановления. Последнее особенно важно для двоичной компиляции, поскольку любое обращение в память можно рассматривать как обращение по указателю.

Статический анализ

В качестве промежуточного представления, порождаемого на выходе после статического анализа, выбран управляющий граф. Линейные участки являются узлами этого графа, а управляющие связи служат дугами. При построении управляющего графа нужно решить задачу поиска преемников линейных участков, содержащих переходы (поиск меток). Условные и безусловные переходы, явные вызовы процедур (команда CALL) содержат адрес относительно счетчика команд, и поэтому легко разрешимы. Но косвенные переходы (команда JMPL) требуют специального анализа.

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

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

Синтез и оптимизации

После статического анализа производится попроцедурная подстановка семантики для исходных SPARC-инструкций в терминах промежуточного представления компилятора "Эльбрус". Предварительно осуществляется отображение окон и регистров SPARC'а в регистровое пространство "Эльбрус" (часть регистров SPARC'a явно реализуется в "Эльбрус" или отображается в области сохранения). Большинство операций SPARC'а имеют отображение в соответствующие операции "Эльбрус". Но некоторые из них (например MULScc) отображаются семантически эквивалентными подграфами. В ряде случаев вызовы подпрограмм, которые могут быть заменены одной операцией "Эльбрус", также заменяются в процессе синтеза.

Далее над полученным промежуточным представлением производятся обычные оптимизирующие преобразования, включая выявление и обработку циклов, глобальный межпроцедурный анализ, слияние линейных участков и т.д. К сожалению, в двоичном коде содержится недостаточно информации для осуществления оптимизаций в полном объеме. Многие информационные связи потеряны из-за размещения данных в памяти. Компилятор "Эльбрус" делает оптимистические предположения, опираясь на специальную аппаратную поддержку и используя программные методы восстановления в случаях, если эти допущения не верны.

К числу таких оптимизаций относится переупорядочение операций чтения/записи в память (при этом последовательность записей сохраняется). Для контроля используется аппаратная ассоциативная таблица и специальные операции работы с ней. Другая оптимизация касается переноса локальных переменных из SPARC-стека в регистры "Эльбрус". Для сохранения корректности и семантики такого преобразования исходные ячейки памяти помечаются специальным маркером, который в случае обращения к ним вызовет аппаратное прерывание. Далее методами динамического анализа можно произвести процесс восстановления и корректно продолжить вычисления.

Еще одна оптимизация касается замены SPARC-адресов в глобальной памяти на адреса в терминах "Эльбруса". В этом случае в дополнительной памяти выделяются ячейки памяти с новой адресной информацией, соответствующие операции чтения/записи, перенаправляются в эту память, а исходные ячейки закрываются аппаратно по доступу к ним. Если какая-либо команда попытается считать такие помеченные данные, произойдет прерывание, которое будет обработано динамическим анализом.

Исполнение двоичного кода

Двоично оттранслированная программа должна быть загружена в адресное пространство таким образом, чтобы оставаться "невидимой" для исходного двоичного кода. Любая операция чтения/записи в адресное пространство, занимаемое кодом "Эльбрус", должна вызывать прерывание. Доступ к собственным данным "Эльбрус" осуществляется при помощи специальных операций.

В процессе исполнения возникают следующие ситуации:

¨  "неизвестный" адрес для косвенного перехода (не обнаруженный на этапе статического анализа);

¨  самомодифицируемый код;

¨  динамическая линковка;

¨  прерывания.

В случае статически неизвестного адреса перехода возникает прерывание при переходе на SPARC-код. После динамического анализа и восстановления мы получаем новую информацию для последующей перекомпиляции и построения локальной таблицы соответствия адресов для такого косвенного перехода.

Наиболее частый случай генерации самомодифицируемого кода – создание его в сегменте данных. Если меняются команды в сегменте кода, то этот случай сводится к первому путем перевода соответствующей страницы в разряд данных. Процедура, код которой подвергся модификации, далее должна исполняться в режиме интерпретации SPARC. Перед переходом на самомодифицируемый код готовится сброс состояния теневых регистров "Эльбрус", соответствующих регистрам SPARC, и далее производится переход на интерпретацию этого кода. При возврате из самомодифицируемого кода производится переключение на код "Эльбрус", который анализирует ситуацию и осуществляет переход на подходящую метку.

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

Асинхронные прерывания также не представляют проблемы, если мы не нарушаем порядок доступа к памяти операциями чтения/записи. В случае перестановки таких операций для корректного решения обработки прерывания также используется аппаратная ассоциативная таблица и спецоперации. Обработчик прерывания запускается в точках согласованного контекста, то есть в тех местах кода, где состояние памяти и регистров SPARC соответствуют реальному исполнению программы.

Случай синхронного прерывания решается путем выполнения операций "Эльбрус" в спекулятивном режиме, и только записи в теневые SPARC-регистры следует производить безусловно [18]. Тем самым всегда имеется корректное состояние, соответствующее аналогичному исполнению программы на SPARC-процессоре (как это делается в аппаратуре в суперскалярных реализациях). Однако такой режим трансляции безусловно снижает эффективность. К счастью, подавляющее большинство реальных программ не устанавливает обработчиков синхронных прерываний или обработчики прерываний заканчиваются вызовом системного вызова "конец задачи". Существует и другая техника обработки синхронных прерываний, которая реализуется алгоритмами планирования и удержания исходных операндов до их переиспользования.

Динамический анализ

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

·         переход на SPARC-код;

·         помеченные локальные и глобальные данные (перенос на регистры);

·         самомодифицируемый код;

·         асинхронные и синхронные прерывания;

·         доступ к драйверу ввода/вывода, загруженному в память;

·         перестановка операций чтения/записи в память;

·         конвертирование параметров при системных вызовах.

В процессе восстановления управление передается динамическому двоичному компилятору SPARC-кода (или осуществляется переход в режим прямого исполнения). Далее происходит возврат в двоично оттранслированный код (код "Эльбрус").

С помощью изложенной в статье схемы двоичной компиляции был оттранслирован ряд приложений для системы SPARC/Solaris, и затем полученный код "Эльбрус" исполнен на интерпретаторе процессора "Эльбрус", эмулирующий операционную среду Solaris. Полностью перенесен весь пакет тестов SPEC-92 (20 тестов), среди них компилятор GCC с языка С, интерпретатор с языка LISP и другие. Часть тестов была оттранслирована с оптимизациями.

Кроме того, были перенесены и исполнены на интерпретаторе реальные большие пользовательские приложения:

à         система проектирования Verilog;

à         компьютерная игра, имитирующая управление самолетом Aviator 1.8;

à         программа демонстрации видеофильмов Movie;

à         интерпретатор команд UNIX Bourne shell и ряд других приложений.

Следует отметить, что после локальной отладки двоичного компилятора все приложения исполнялись практически без ошибок.

Рассмотренная в статье технология двоичной компиляции позволяет говорить о возможности создания микропроцессора с множественной системой команд MISC (Multiple Instruction Set Computer). В настоящее время разработанный командой создателей машины "Эльбрус-3" микропроцессор "Эльбрус" ориентирован на исполнение в режиме двоичной компиляции кодов Intel x86, при этом он полностью совместим с процессорами фирмы Intel. В микропроцессоре также имеется возможность исполнения кодов и других архитектур (в том числе и Merced).

Список литературы

1. Apiki S. Windows on RISC. BYTE, vol.19, No.4, April 1994, pp.109-115.

2. Halfhill T. Emulation: RISC's Secret Weapon. BYTE, vol. 19, N. 4, April 1994, pp. 119-130.

3. Sites R., Chernoff A., Kirk M., Marks M. and Robinson S. Binary Translation. Communications of the ACM, vol.36, No.2, February 1993, pp. 69-81.

4. FreePort Express. http://www.digital.com/semiconductor/amt/freeport/index.html

5. Chernoff A. et al. FX!32: A Profile-Directed Binary Translator. IEEE Micro, v.8, No.2, March/April 1998, pp. 56-64.

6. Ramakrishna B. Rau David W.L. Yen Wei Yen and Ross A. Towle. The Cyrda 5 Departmental Supercomputer: Design Philosophies, Decisions, and Trade-offs. Computer, vol. 22, No. 1, January 1989, pp. 12-35.

7. Бабаян Б.А., Волконский В.Ю. и др. Высокопроизводительный центральный процессор МВК Эльбрус-3 с архитектурой широкого командного слова //Семейство моделей многопроцессорных вычислительных комплексов "Эльбрус": состояние и перспективы //Тез. Докл. Всесоюз. науч.-техн. сем. НПО СВТ, ИТМ и ВТ АН СССР, ИЦ НПО АСУ. - М., 1990. - С. 24-25.

8. Intel, HP Make EPIC Disclosure, Microprocessor Report, vol.11, No.14, October 27, 1997, p.1

9. Hollander C. Decompilation of Object Programs. Stanford University, Ph.D., 1973.

10. Friedman F. Decompilation and the Transfer of Mini-Computer Operating Systems. Purdue University, Ph.D., 1974.

11. Cifuentes C. and Gough KJ. Decompilation of Binary Programs. Software - Practice & Experience. vol.25 (7), July 1995, pp. 811-829.

12. Larus J. and Schnarr E. EEL: Machine-Independent Executable Editing. SIGPLAN Notices, vol.30, No.6, 1995, pp. 291-300.

13. Bochs x86 PC Emulation Software. http://world.std.сom /~bochs

14. Cmelik R. and Keppel D. Shade: A Fast Instruction-Set Simulator for Execution Profiling. SUN Microsystems Laboritaries Tech. Report, 1993.

15. Boris Babaian, Vladimir Volkonsky et al. Wide Instruction Word Architecture Central Processor. United States Patent N 5418975, May 23, 1995; PCT Pub. Date: Oct. 15, 1992; PCT Filed: Aug. 20, 1991.

16. Alexandr Drozdov, Vladimir Volkonski et al. The Optimizing Compiler for the Elbrus-3 Supercomputer. International Congress on Computer Systems and Applied Mathematics, CSAM'93, Abstracts, St. Petersburg, July 19-23, 1993, pp.127-128.

17. Weaver D. and Germond T. The SPARC Architecture Manual (Version 9). PTR Prentice Hall, New Jersey, 1994.

18. Silberman G. and Ebcioglu K. An Architectural Framework for Supporting Heterogeneous Instruction-Set Architectures. IEEE Computer, vol.26, No.6, 1993, pp. 39-56.


Постоянный адрес статьи:
http://swsys.ru/index.php?page=article&id=912
Версия для печати
Выпуск в формате PDF (1.25Мб)
Статья опубликована в выпуске журнала № 1 за 1999 год.

Назад, к списку статей