Анализируют ли эмуляторы двоичный код внутри файлов?

Анализируют ли эмуляторы двоичный код внутри файлов?

Я видел несколько эмуляторов, которые заявляли, что выполняют инструкции, и даже если это так, их исходный код показывает, что они не анализируют напрямую каждую 1 и 0, чтобы определить инструкцию.

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

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

0000 1111

Моя программа должна проверить, что эта инструкция действительно означает (например, «прибавить единицу к регистру A»), но разве не нужно будет проверить каждый ноль и единицу в текстовом файле, чтобы убедиться в этом?

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

Например, 0000 1111 может означать добавление единицы к A, а 0000 1110 может означать добавление A к A.

решение1

Экспозиция — попытка прямого ответа на вопрос

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

  1. Тынеправильныйдумая, что эмулятор не читает каждый бит файла, и онна самом деле делает, и вы просто ошибаетесь.
  2. Тыправильный, и эмулятор не считывает каждый отдельный бит, потому что он может предполагать определенные факты о поведении эмулируемой им программы, чтобы ему не нужно было считывать каждый отдельный бит, чтобы знать, что ему нужно сделать (возможно, потому что он ожидает запуска определенного игрового движка, или определенного типа графического API, или определенного типа звукового API и т. д.).
  3. Тыправильный, и эмулятор не считывает каждый бит, потому что есть определенные биты исполняемого файла, которые просто не нужны для корректного выполнения программы. Это может быть устаревший "хлам", или метаданные, или что-то еще, что является просто лишним хламом, который на самом деле не состоит из функциональности программы.
  4. Тыправильный, и эмулятор не считывает каждый бит, потому что эмулятор преобразует определенные операции в коде в операции более высокого уровня и полностью обходит низкоуровневые инструкции, специфичные для процессора/оборудования. Например, если вас просят точно воспроизвести то, что делает человек в видеозаписи, где этот человек выполняет сложную операцию, и они говорят: «А теперь просверлите отверстие в боковой стороне коробки», у вас возникнет соблазн прекратить просмотр видео и использовать свой существующий опыт сверления отверстий вместо того, чтобы следовать буквальным движениям парня в видео (предполагая, что у вас есть подходящая дрель и в целом вы имеете жизненный опыт). Аналогично, если эмулятор может сделать вывод, что программа просит нарисовать изображение 32x32 на экране в заданном наборе координат, он может прекратить чтение кода, как только поймет, что это за изображение, в каком формате оно находится и где его рисовать — ему не нужно видеть, как его рисует эмулируемая система.

Как работают эмуляторы

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

  • Необходимый: «Разбор» исполняемого кода (машинный код, MSIL, байт-код Java и т. д.)Разборсостоит из:

    • Чтение каждого бита исполняемого кода.
    • Достаточно хорошо понимать структуру/формат (синтаксис) и назначение (семантику) каждого бита/байта (или любой другой дискретной единицы измерения информации, которую вы захотите использовать) собственного кода, чтобы понимать, что он делает.
    • Пониматьчтопрограмма говорит, эмулятор должен пониматьсинтаксисдвоичного формата исемантика. Синтаксис состоит из таких вещей, как «мы выражаем 32-битные целые числа со знаком в формате Least Signed Bit»; семантика состоит из таких вещей, как «когда собственный код содержит код операции52, это означает сделать вызов функции.
    • Мнемонический(помогает вам вспомнить, почему это необходимо): Если я посвятил себя следованию рецепту, если я полностью проигнорирую этот рецепт и даже нечитатьэто, невозможно, что я когда-либо смогу следовать этому рецепту, если только я не попробую наугад кучу вещей иудачав выполнении тех же шагов, которые требуются в рецепте. Аналогично, если у вас нет рандомизированного моделирования Монте-Карло, которое выполняет случайные инструкции ЦП, пока ему не повезет, выполняя ту же функциональность, что и программа, любой эмулятор должен будет пониматьчтоговорится в программе.

  • Необходимый: «Трансляция» проанализированного кода (обычно это некая абстрактная модель данных, конечный автомат, абстрактное синтаксическое дерево или что-то в этом роде) ввысокий уровенькоманды (например, операторы на языке C или Java) илинизкий уровенькоманды (например, инструкции ЦП для процессора x86). Высокоуровневые команды, как правило, более оптимальны. Например, если вы проанализируете поток кода длинной последовательности инструкций ЦП и определите на высоком уровне, что он запрашивает воспроизведение определенного файла MP3 с диска, вы можете пропустить всю эмуляцию на уровне инструкций и просто использовать декодер MP3 вашей собственной платформы (который может быть оптимизирован для вашего процессора) для воспроизведения того же файла MP3. С другой стороны, если бы вы «отслеживали» выполнение эмулируемой программы настолько буквально, насколько это возможно, это было бы медленнее и менее оптимально, потому что вы бы отказались от большей части оптимизации, которую вы получаете, выполняя инструкции нативно.

  • Необязательный: "Оптимизация" и анализ потока кода большого участка эмулируемого программного кода или всей программы для определения полной последовательности выполнения и построения очень подробной и сложной модели того, как ваш эмулятор будет эмулировать это поведение с помощью возможностей собственной платформы. Wine делает это в некоторой степени, но ему помогает тот факт, что код, который он транслирует, является x86-в-x86 (это означает, что в обоих случаях ЦП имеет один и тот же набор инструкций, поэтому все, что вам нужно сделать, это подключить код Windows к внешней среде на основе UNIX и позволить ему работать "в собственном" режиме).


Аналогия с тортом

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

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

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

  • Если вы никогда в жизни не пекли торт, но раньше занимались приготовлением пищи;(подсказка: вам понадобится пара листов бумаги, но не больше 10; вы уже знакомы с измерением ингредиентов, помешиванием и т. д.)

  • Если вы уже много раз пекли торты и хорошо знакомы с процессом, но не знаете, как испечь именно этот сорт/вкус торта(подсказка: вам может понадобиться поллиста бумаги, чтобы записать основные ингредиенты и время, необходимое для выпекания в духовке, и все).

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

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


О «трассировке» (она же механическая имитация) и «исполнении нативных кодов»

И последнее: трассировка медленная, в основном, потому, что вам приходится использовать большой объем памяти для «репликации» очень подробных, сложных компонентов эмулируемого вами устройства, и вместо того, чтобы просто выполнять инструкции на вашем главном процессоре, вам приходится выполнятьинструкции, которые выполняют инструкции(видите уровень косвенности?), что приводит к неэффективности. Если бы вы пошли напролом и эмулировали физическое оборудование компьютерной системы, а также программу, вы бы эмулировали ЦП, материнскую плату, звуковую карту и т. д., которые в свою очередь «отслеживали» бы выполнение программы, как ваш эмулятор «отслеживает» выполнение ЦП, и с таким количеством уровней трассировки все это было бы чрезвычайно медленным и громоздким.

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

Допустим, мы знаем API, написанный на C или C++ (детали не важны) для эмулируемой программной среды, где этот API имеет функцию void playSound(string fileName). Допустим, мы знаем, что семантика этой функции заключается в том, чтобы открыть файл на диске, прочитать его содержимое, выяснить, в какой кодировке находится файл (MP3? WAV? что-то еще?), а затем воспроизвести его на динамиках с обычной/ожидаемой частотой дискретизации и высотой тона. Если мы читаем из собственного кода набор инструкций, который говорит «войти в процедуру playSound, чтобы начать воспроизведение звука /home/hello/foo.mp3», мы можем прекратить чтение программного кода прямо здесь и использовать нашсобственный(оптимизированная!) процедура для нативного открытия этого звукового файла и его воспроизведения. Нужно ли нам следовать за эмулируемым процессором на уровне инструкций? Нет, нам действительно не нужно, если мы верим, что знаем, что делает API.


Возникает дикая разница! (проблема на высокогорной земле)

Конечно, читая кучу инструкций и «выводя» высокоуровневый план выполнения, как в примере выше, вы рискуете, что не сможетеименно такимитировать поведение оригинальной программы, работающей на оригинальном оборудовании. Например, оригинальное оборудование могло иметь аппаратные ограничения, которые позволяли ему воспроизводить только 8 звуковых файлов одновременно. Ну, если ваш новый компьютер может прекрасно воспроизводить 128 звуковых файлов одновременно, и вы эмулируете процедуру playSoundна высоком уровне, что мешает вам воспроизводить более 8 звуковых файлов одновременно? Это может вызвать... странное поведение (к лучшему или к худшему) в эмулируемой версии программы. Эти случаи можно разрешить путем тщательного тестирования или, возможно, путем действительно хорошего понимания исходной среды выполнения.

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

решение2

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

«Разбирать» означает «пройтись по тексту и выяснить, что он означает». Текстовый и языковой синтаксис сложен, поскольку не только отдельные «слова» что-то означают, но и их значение может меняться в зависимости от их положения и близости к другим словам.

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

Моя программа должна проверить, что эта инструкция действительно означает (например, «прибавить единицу к регистру A»), но разве не нужно будет проверить каждый ноль и единицу в текстовом файле, чтобы убедиться в этом?

Процессоры на самом деле не проверяют инструкции. У любого процессора есть внутренний регистр, который указывает на ячейку памяти в ОЗУ. Процессор считывает его, считывает еще пару байтов, если нужно, а затем пытается выполнить его. Современные процессоры запускают «процесс исключения», если инструкция недопустима.

Старые процессоры, такие как старый 8-битный 6502, даже этого не делали — некоторые незаконные инструкции блокировали процессор, другие делали странные, недокументированные вещи. (Некоторый поиск выявит ряд недокументированных инструкций x86, обнаруженных во всех процессорах в пантеоне x86 — например, эта CPUIDинструкция существовала в некоторых процессорах 486 до того, как Intel официально задокументировала ее.)

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

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

Связанный контент