Самый популярный вопрос о моём эмуляторе Windows retrowin32 (после «Зачем вообще это нужно?») — это вопрос о том, как он работает. Сегодня ответ кажется мне очевидным, но прежде чем я разобрался, он представлял для меня огромную загадку. Поэтому я постараюсь объяснить так, чтобы вам тоже стало понятно.

Эмуляция Windows API


Для начала представьте, что вы работаете с машиной x86, на которой установлена какая-то операционная система (не Windows), и вам нужно как-то запустить программу для Windows. Основной вывод из наблюдения за Wine (который, как следует из расшифровки аббревиатуры Wine Is Not Emulator, не является эмулятором) заключается в том, что исполняемый файл Windows в конечном итоге содержит последовательность команд x86, а ваша машина x86 уже способна напрямую исполнять их. Значит, для исполнения файла .exe Windows вам достаточно просто загрузить его в память (для чего нужно распаковать формат файлов .exe) и приказать процессору перейти к первой команде.

Единственное, что остаётся (и это очень масштабная задача) — способ взаимодействия этого exe с операционной системой, например, как он открывает файлы или выводит что-то на экран. Механизм сильно отличается для разных операционных систем, но в конечном итоге зависит от интерфейса ядра. В конкретном случае Windows интерфейс ядра довольно замороченный (идентификаторы системных вызовов различаются для разных версий Windows), и обычно считается, что стабильная граница API находится в DLL со знакомыми вам именами наподобие kernel32.dll. (Это является полной противоположностью Linux, который известен своим вниманием к наличию очень стабильного интерфейса на границе ядра.)

Это работает следующим образом: формат файлов .exe может объявить: «эй, мне нужно будет вызвать функцию kernel32.dll с именем WriteFile()», и когда .exe загружается, операционная система помещает соответствующий код в соответствующее место так, чтобы вызов функции сработал. Затем в Windows функция kernel32 вызывает соответствующий интерфейс ядра. Это удобно для выполнения нашей цели запуска файла в ОС, отличной от Windows, потому что нам достаточно лишь предоставить собственные реализации этих функций, даже не обращая внимания на интерфейс ядра.

Именно так и поступает Wine: он загружает файлы .exe и предоставляет реализации всех DLL Windows. Естественно, на практике всё существенно сложнее, и Wine потребовались века человеко-часов работы программистов, чтобы воспроизвести все особенности и странности интерфейса Windows, который сам десятки лет подвергался воздействию закона Хайрама.

Один из примеров того, насколько глубока может быть кроличья нора, можно увидеть в этом фрагменте, находящемся в блобе ассемблерного кода x86 диспетчера системных вызовов Wine:

/* Legends of Runeterra подменяет первую команду возврата системного вызова,
 * и ожидает, что туда перейдёт выполнение. Изменяем адрес возврата соответствующим образом. */
"subq $0xb,0x70(%rcx)\n\t"

В отличной статье how Wine works ещё подробнее рассказывается о Wine. Я намеренно опустил множество деталей.

(Кстати, у Wine есть один интересный способ применения, никак не связанный с задачей этого поста — если у вас есть исходный код программы для Windows, то можно скомпилировать его для созданной в Wine реализации Windows API и получить на выходе нативный исполняемый файл.)

Эмуляция x86


Всё описанное выше отлично работает на оборудовании x86, но что если вы на какой-то другой архитектуре, например, на новых Mac с процессорами ARM? Тогда вам нужно эмулировать набор команд x86 точно так же, как эмулятор Game Boy может эмулировать процессор Game Boy.

А это непростая задача! Apple даже добавила в свои процессоры ARM специальную поддержку x86, чтобы ускорить эмуляцию. Но после того, как вы справитесь, то дальше можете пойти по двум разным путям.

Первый: дополнительно эмулировать всё оборудование, находящееся в машине x86, например, BIOS и интерфейсы дисков, так, чтобы можно было установить в ваш эмулятор настоящую ОС Windows. Этот подход используется в qemu. Есть также веб-эмулятор v86, способный запускать множество разных ОС, в том числе и Windows.

Такой подход прекрасен тем, что запускается настоящая копия Windows; следовательно, все программы для Windows будут работать корректно. Основной недостаток этого подхода в том, что необходимо устанавливать реальную копию Windows, что требует много пространства на диске и времени на запуск Windows, прежде чем приступать к исполнению программ.

Второй подход: эмулировать набор команд x86 и использовать описанный выше Wine в качестве реализации огромного Windows API, основывающегося на чём-то более удобном для эмуляции, например, на API ядра Linux. После публикации retrowin32 я узнал о BoxedWine, который выполняет эту задачу в вебе и способен запускать множество сложных программ для Windows.

Подход retrowin32


Мой проект retrowin32 в основном предназначен для изучения того, что мне кажется интересным. На текущий момент это значит, что у меня есть не особо хороший эмулятор x86, не особо хорошая реализация win32 и исследовано несколько близких по теме задач.

В целом, я думаю, что если вам хочется запустить программу для Windows в вебе, то, скорее всего, лучше всего подойдут BoxedWine и v86. Но если вам любопытны мои исследования, то ждите следующего поста, в котором я напишу о текущем состоянии retrowin32.

Комментарии (3)


  1. MiraclePtr
    25.01.2023 12:50
    +3

    Мне ещё очень HX DOS Extender понравился. Это типа Wine (реализация Win32 API), только под DOS. Эмклирует KERNEL32, USER32, GDI32, OLE32, и даже DirectX (DINPUT, DDRAW и т.д.) и OPENGL32. Умеет запускать многие консольные Win32-приложения, типа Far Manager, ffmpeg, MPlayer, 7-zip, некоторые игры (например, оригинальный Quake 2 и ScummVM), можно даже запустить QEMU или DosBox (DOS внутри DOS, ага), и кучу всего другого. Графические приложения некоторые запускать пытается, но там почти ничего не реализовано, в лучшем случае покажет форму с парой кнопок :)

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


  1. mayorovp
    25.01.2023 13:05
    +2

    Legends of Runeterra обрабатывает первую команду возврата системного вызова,
    и ждёт, что мы вернём его. Изменяем адрес возврата соответствующим образом.

    Чего-чего оно делает? Как вообще прикладная программа может обрабатывать команду процессора?


    Ну нельзя же так переводить! В оригинале использовать глагол "hooks", что означает что LoR подменяет первую команду возврата и ожидает что туда перейдёт управление.


    1. PatientZero Автор
      25.01.2023 13:42
      +1

      Спасибо, исправил