Самый популярный вопрос о моём эмуляторе 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)nt"
В отличной статье 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.
Автор:
PatientZero