О разработке прототипа эмулятора WIE: запуск Windows PE64 бинарников на Apple Silicon

от автора

Здравствуйте. Меня зовут Владислав, и я хочу рассказать о своем экспериментальном проекте на ранней стадии. Называется он WIE (Wie Is Emulator), что отсылает к великим.

WIE — это исследовательский эмулятор пользовательского режима (userspace) для запуска 64-битных Windows‑приложений (PE64) на архитектуре macOS Apple Silicon. Проект написан на Rust 1.97, а в качестве бэкенда компиляции используется Cranelift для трансляции x86-64 инструкций на лету в нативный ARM64.

Главный Proof of Concept на сегодня: эмулятор успешно крутит под честным JIT реальный консольный Windows 7-Zip (7za.exe), выполняя сжатие и распаковку с полным совпадением SHA-256 хэшей. А также для тестирования было разработано более 20 тестовых EXE файлов на различные задачи: От математики и долгих циклов, заканчивая проверкой ввода и корректности многопоточности

Что не планируется

  • 32-битные и 16-битные приложения

  • Никакой полной истории от Windows от 95 до 11. Только Windows 10 и совместимые PE64 со старых версий

  • Мягкая трансляция памяти (Soft‑translate): принципиально не использую Wine‑style identity mapping (mmap(addr = guest_va)). Гостевые адреса изолированы и всегда транслируются через софтверные таблицы регионов, арены, структуры VAD и PageMap.

Использование ИИ

«Да ладно, на сайт пришел очередной вайбкодер!» — скажете вы, и в принципе я с вами соглашусь. Значительная часть кода проекта действительно сгенерирована нейросетями, и я этого не скрываю. Но должен сразу уточнить: этот проект не из тех, где «запустил 12 ИИ‑агентов на ночь и получил убийцу Wine».

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

За время работы над разными приватным проектами я научился глубоко читать и понимать Rust: какие конструкции за что отвечают, как взаимодействовать с компилятором и банально как правильно подходить к работе с нейросетями. Ведь всем «нейрокодерам» известно — без жесткой и правильной инструкции ИИ моментально уйдет не туда.

Также я бьюсь за чистоту кода. Мне важно досконально понимать, что происходит в системе. Когда в проекте скапливается куча legacy‑мусора, контролировать логику становится тяжело не только мне, но и самой нейросети.

И, естественно, unsafe. Главный мрак Rust. Практически весь unsafe, который есть в WIE, локализован строго в блоке ядра CPU. Есть пара исключений в блоке WinAPI, но на этом всё. В остальном проект глобально настроен на безопасный код.

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

Начало конца

Лично мне никогда не приходилось мучаться с Wine, так как лично мне Windows приложения не нужны были. Я не геймер, так что главная причина использовать отпадает, ничего специфичного не надо было.

Но вот в один момент меня что‑то потянуло на ретроигры, а вернее довольно интересную сферу — Ромхакинг. А точнее Super Mario World и легендарный редактор Lunar Magic. Я когда начал изучать эту тему очень сильно удивился: Ради того чтобы переделать оригинальный ROM файл за примерно 2 десятилетия сообщество придумало такое количество способов расширить файл и настолько перерабатывать игру, что просто жуть.

Мне стало интересно как устроен Lunar Magic авторства FuSoYa, который является одним из главнейших столбов. Кратко: есть полно утилит на отдельную часть (музыка, графика, кастомные предметы) и они разрозненны, а Lunar является сборщиком упаковщиком с понятным графическим интерфейсом. Но есть деталь… Практически все ромхакерское ПО исключительно под Windows и только пара есть и на macOS. Конкретно Lunar у меня спокойно в Wine запустится, но другие у многих вызывают проблемы. Но сначала именно Lunar меня заинтересовал именно из‑за скрепления и понимания всего.

И я предпринял попытку нейросетевого реверс‑инжиниринга и портирования редактора на Rust… Что тут сказать: эта попытка хоть и неплохо начиналась, но с треском рухнула на этапе рендеринга. Чтобы вы понимали масштабы бедствия: более трех тысяч функций в EXE‑файле весом в несколько мегабайт.

Однако меня не отпускало. Тянуло попробовать запустить это на Mac без Wine. Я искал разные способы, но только один показался более‑менее реалистичным: написать заглушки под WinAPI, а сам гостевой EXE‑файл скормить в эмулятор Unicorn Engine. С этого всё и началось. Я генерировал и генерировал заглушки, которые просто пропускали выполнение по инициализации, но почти ничего общего с реальными функциями kernel32 и прочих библиотек не имели.

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

Но долго без дела сидеть не смог — не получалось найти занятие по душе. Мне хотелось создать что‑то уникальное, ведь рынок IT, как известно, перенасыщен стандартными решениями. Проверяя свои старые репозитории в попытке найти то, что можно реанимировать, я всё‑таки вернулся к этой идее. Но на этот раз я полностью поменял перспективу. Вместо попытки запустить один конкретный Lunar Magic я принял решение замахнуться на невозможное: переработать проект так, чтобы он мог запускать самые разные EXE‑файлы. По сути, сделать аналог Wine.

Но Wine — это не эмулятор! Это транслятор системных вызовов. А у тебя под капотом именно эмуляция процессора

Так и родилось название WIE — Wie Is Emulator.

Судьбоносная замена

Unicorn сначала казался неплохим. Он был удобен в плане анализа и просто проект изначально под него строился. Но теперь две проблемы которая перекрывает все хорошее: CPU и скорость.

Как‑бы ты не пытался снижать потребление CPU, ты всегда будешь больше тратить чем Wine. Я конечно начал попытку оптимизаций. В какой‑то момент ИИ предлагал варианты оптимизаций один из вариантов был переход на Cranelift + iced‑x86. Я заинтересовался, так как он написал что он легковеснее, JIT движок быстрее чем у Unicorn и вообще предназначен изначально под веб. Решил переходить на него.

Замена Unicorn на Cranelift правда‑сказать сожгла недельные лимиты. И даже не столько замена, сколько попытка ускорить и оптимизировать минимально. В какой то момент на тестах еще сохранившегося в проекте Lunar Magic, который уже проходил цикл инициализации полностью в один момент скорость cranelift превзошла Unicorn почти на 2 секунды.

Вообще кстати они оба со временем ускорялись все больше и больше. Начиналось вроде где‑то с 20 секунд, а закончилось от семи до десяти (если я не ошибаюсь). После этого я решил что пора вычеркивать lunar из проекта: Все переменные, адреса, функции подстроенные только под него, а также Unicorn Engine, который я успешно на Cranelift и iced‑x86. Вот после этого момента можно и сказать, что проект начал принимать облик нынешнего варианта.

А как устроено?

Если отбросить дальнейший путь разработки и посмотреть на WIE (Wie Is Emulator) сегодня, то это модульный, легковесный эмулятор пользовательского режима, написанный на Rust. Его архитектура разделена на четыре ключевых блока, которые работают в тесной связке:

  • wie‑pe (Загрузчик): Берет гостевой 64-битный Windows‑бинарник, парсит его структуру, маппит секции в изолированную хост‑память и полностью переписывает Таблицу импортов (IAT). Все системные вызовы Windows подменяются на кастомные виртуальные адреса‑ловушки.

  • wie‑winapi (Прослойка окружения): Та самая поверхность WinAPI, которую мы итеративно воссоздаем под нужды приложений. Чтобы не прыгать в контекст хоста по малейшему поводу, базовые и часто вызываемые функции (например, работа с ошибками вроде GetLastError или SetLastError) имеют инлайновые заглушки прямо в гостевой памяти.

  • wie‑cpu (Движок компиляции): Сердце проекта. С помощью iced-x86 декодируются инструкции x86-64, собираются в базовые блоки и передаются JIT‑бэкенду Cranelift, который на лету превращает их в нативный ARM64-код, оптимизированный под Neon‑векторы Apple Silicon. 

  • wie‑cli (Командный пункт): Место откуда идет управление, слежка, шпионаж… Проще говоря просто CLI блок с тремя командами. trace, run и inspect

Архитектура памяти

Почему Soft‑translate, а не подход Wine? Он использует подход Identity Mapping, когда гостевой адрес пытается напрямую отобразиться на аналогичный адрес хоста через mmap(addr = guest_va).

В WIE реализована мягкая трансляция памяти (Soft‑translate). Гостевое пространство полностью изолировано. Каждый адрес гостя — это виртуальная абстракция, которая принудительно транслируется через софтверные таблицы регионов, арены, структуры VAD (Virtual Address Descriptor) и PageMap. Да, это накладывает свои накладные расходы, но дает тотальный контроль над правами страниц и безопасностью выполнения. 

Многопоточность

Эмуляция многопоточности — это отдельная тема. В WIE гостевые потоки, создаваемые через CreateThread, маппятся на реальные системные потоки хоста (macOS pthreads) в соотношении 1:1. Для синхронизации используется глобальный мьютекс процессора (CpuEngine process mutex). Когда гостевой поток уходит в законное ожидание (например, через WaitForSingleObject), блокировка движка CPU освобождается, предотвращая холостой простой хост‑системы.

Конечно это все хорошо, но что в итоге? Что угодно запустит?

Нет! Это очень ранний прототип. Но это не значит что он ничего не умеет. Как говорил в процессе разработке было создано более 20 EXE. Далее будут примеры:

Пример 1: Цикл

#include <windows.h>void entry(void) {  volatile unsigned long long counter = 0;  volatile unsigned long long limit = 100000000ULL;  if (counter < limit) {    do {      volatile unsigned long long tmp = counter ^ 0xDEADBEEF;      tmp = tmp * 3 + 1;      (void)tmp;      counter++;    } while (counter < limit);  }  ExitProcess(0);}

Это был первый крупный тест. До оптимизации он проходил за 9 секунд, теперь:

time ./target/release/wie-cli run micro-exes/out/long_loop.exerun_micro: path=micro-exes/out/long_loop.execpu_backend: jitentry=0x0000000140001000 initial_rsp=0x000000002000eff8events=1 termination=ExitProcess { code: 0 }  [   0] KERNEL32.dll!ExitProcess handled=true ret=Nonerun_micro: ok exit=0./target/release/wie-cli run micro-exes/out/long_loop.exe  0.31s user 0.01s system 99% cpu 0.325 total

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

Пример 2: Тяжелая многопоточность

#include <windows.h>#define WORKERS 4#define ITERS   512typedef LONG(WINAPI *PFN_Inc)(LONG volatile *);static CRITICAL_SECTION g_cs;static volatile LONG g_atomic = 0;static volatile LONG g_under_cs = 0;static volatile LONG g_slots[WORKERS];static HANDLE g_start_event;static HANDLE g_done_event;static volatile LONG g_ready = 0;static PFN_Inc g_inc;static DWORD WINAPI worker(LPVOID param) {    int id = (int)(ULONG_PTR)param;    int i;    HANDLE heap;    LONG marker = 0x1000 + id;    if (g_inc(&g_ready) == WORKERS) {        SetEvent(g_done_event);    }    WaitForSingleObject(g_start_event, INFINITE);    heap = GetProcessHeap();    for (i = 0; i < ITERS; i++) {        void *p;        g_inc(&g_atomic);        EnterCriticalSection(&g_cs);        g_under_cs++;        p = HeapAlloc(heap, 0, 64);        if (p) {            *((volatile LONG *)p) = marker;            HeapFree(heap, 0, (LPVOID)p);        }        LeaveCriticalSection(&g_cs);        g_slots[id] = marker;    }    ExitThread(0);    return 0;}void entry(void) {    HANDLE threads[WORKERS];    DWORD wait;    int i;    HMODULE k;    const LONG expect = (LONG)(WORKERS * ITERS);    for (i = 0; i < WORKERS; i++) {        g_slots[i] = 0;    }    k = GetModuleHandleA("KERNEL32.dll");    if (!k) {        k = GetModuleHandleA("kernel32.dll");    }    if (!k) {        ExitProcess(6);    }    g_inc = (PFN_Inc)GetProcAddress(k, "InterlockedIncrement");    if (!g_inc) {        ExitProcess(6);    }    InitializeCriticalSection(&g_cs);    g_start_event = CreateEventA(NULL, TRUE, FALSE, NULL); /* manual */    g_done_event = CreateEventA(NULL, TRUE, FALSE, NULL);    if (!g_start_event || !g_done_event) {        ExitProcess(6);    }    for (i = 0; i < WORKERS; i++) {        threads[i] = CreateThread(NULL, 0, worker, (LPVOID)(ULONG_PTR)i, 0, NULL);        if (threads[i] == NULL || threads[i] == INVALID_HANDLE_VALUE) {            ExitProcess(1);        }    }    wait = WaitForSingleObject(g_done_event, 30000);    if (wait != WAIT_OBJECT_0) {        ExitProcess(6);    }    SetEvent(g_start_event);    for (i = 0; i < WORKERS; i++) {        wait = WaitForSingleObject(threads[i], INFINITE);        if (wait != WAIT_OBJECT_0) {            ExitProcess(2);        }        CloseHandle(threads[i]);    }    if (g_atomic != expect) {        ExitProcess(3);    }    if (g_under_cs != expect) {        ExitProcess(4);    }    for (i = 0; i < WORKERS; i++) {        if (g_slots[i] != (LONG)(0x1000 + i)) {            ExitProcess(5);        }    }    DeleteCriticalSection(&g_cs);    CloseHandle(g_start_event);    CloseHandle(g_done_event);    ExitProcess(0);}

Результат:

time ./target/release/wie-cli run micro-exes/out/mt_stress.exerun_micro: path=micro-exes/out/mt_stress.execpu_backend: jitentry=0x00000001400010e0 initial_rsp=0x000000002000eff8events=17 termination=ExitProcess { code: 0 }  [   0] kernel32.dll!getmodulehandlea handled=true ret=Some(1627389952)  [   2] kernel32.dll!initializecriticalsection handled=true ret=Some(0)  [   3] KERNEL32.dll!CreateEventA handled=true ret=Some(2147483649)  [   4] KERNEL32.dll!CreateEventA handled=true ret=Some(2147483650)  [   5] KERNEL32.dll!CreateThread handled=true ret=Some(2147483651)  [   6] KERNEL32.dll!CreateThread handled=true ret=Some(2147483652)  [   7] KERNEL32.dll!CreateThread handled=true ret=Some(2147483653)  [   8] KERNEL32.dll!CreateThread handled=true ret=Some(2147483654)  [  10] KERNEL32.dll!SetEvent handled=true ret=Some(1)  [  12] kernel32.dll!closehandle handled=true ret=Some(1)  [  14] kernel32.dll!closehandle handled=true ret=Some(1)  [  16] kernel32.dll!closehandle handled=true ret=Some(1)  [  18] kernel32.dll!closehandle handled=true ret=Some(1)  [  19] kernel32.dll!deletecriticalsection handled=true ret=Some(0)  [  20] kernel32.dll!closehandle handled=true ret=Some(1)  [  21] kernel32.dll!closehandle handled=true ret=Some(1)  [  22] KERNEL32.dll!ExitProcess handled=true ret=Nonerun_micro: ok exit=0./target/release/wie-cli run micro-exes/out/mt_stress.exe  0.05s user 0.07s system 163% cpu 0.074 total

Как вы видите: 163% CPU, то есть больше одного потока, 74 миллисекунды всего, а exit 0

Пример 3: Запуск CLI версии 7zip

Предупреждение: Поверхность WinAPI для 7-Zip огромна, и проект находится в стадии наполнения, поэтому проверен далеко не весь функционал утилиты. Для демонстрации мы берем оригинальный, немодифицированный консольный Windows‑бинарник 7za.exe (из официального пакета 7-Zip Extra x64) и запускаем его в изолированном окружении («бутылке»). На данный момент эмулятор стабильно запускает пять команд:

  • a (сжатие) — упаковка файлов в формат.7z, проверенная как в один поток, так и в многопоточных режимах (-mmt2 / -mmt4).

  • x(распаковка) — извлечение файлов с полным сохранением путей.

  • l (листинг) — чтение структуры и вывод содержимого архива.

  • i (инвентаризация) — проверка доступных системе кодеков и хэшеров.

  • help — вывод списка команд.

Finished `release` profile [optimized] target(s) in 0.05sblob.bin 262144zsh: command not found: #bottle_root: /var/folders/16/y3g8vpb14db0rg6g632rl1_m0000gn/T//wie-7za-bottle-71495guest_args: ["--help"]7-Zip (a) 26.02 (x64) : Copyright (c) 1999-2026 Igor Pavlov : 2026-06-25Usage: 7za <command> [<switches>...] <archive_name> [<file_names>...] [@listfile]<Commands>  a : Add files to archive  b : Benchmark  d : Delete files from archive  e : Extract files from archive (without using directory names)  h : Calculate hash values for files  i : Show information about supported formats  l : List contents of archive  rn : Rename files in archive  t : Test integrity of archive  u : Update files to archive  x : eXtract files with full paths<Switches>  -- : Stop switches and @listfile parsing  -ai[r[-|0]][m[-|2]][w[-]]{@listfile|!wildcard} : Include archives  -ax[r[-|0]][m[-|2]][w[-]]{@listfile|!wildcard} : eXclude archives  -ao{a|s|t|u} : set Overwrite mode  -an : disable archive_name field  -bb[0-3] : set output log level  -bd : disable progress indicator  -bs{o|e|p}{0|1|2} : set output stream for output/error/progress line  -bt : show execution time statistics  -i[r[-|0]][m[-|2]][w[-]]{@listfile|!wildcard} : Include filenames  -m{Parameters} : set compression Method    -mmt[N] : set number of CPU threads    -mx[N] : set compression level: -mx1 (fastest) ... -mx9 (ultra)  -o{Directory} : set Output directory  -p{Password} : set Password  -r[-|0] : Recurse subdirectories for name search  -sa{a|e|s} : set Archive name mode  -scc{UTF-8|WIN|DOS} : set charset for console input/output  -scs{UTF-8|UTF-16LE|UTF-16BE|WIN|DOS|{id}} : set charset for list files  -scrc[CRC32|CRC64|SHA256|SHA1|XXH64|*] : set hash function for x, e, h commands  -sdel : delete files after compression  -seml[.] : send archive by email  -sfx[{name}] : Create SFX archive  -si[{name}] : read data from stdin  -slp : set Large Pages mode  -slt : show technical information for l (List) command  -snh : store hard links as links  -snl : store symbolic links as links  -sni : store NT security information  -sns[-] : store NTFS alternate streams  -so : write data to stdout  -spd : disable wildcard matching for file names  -spe : eliminate duplication of root folder for extract command  -spf[2] : use fully qualified file paths  -ssc[-] : set sensitive case mode  -sse : stop archive creating, if it can't open some input file  -ssp : do not change Last Access Time of source files while archiving  -ssw : compress shared files  -stl : set archive timestamp from the most recently modified file  -stm{HexMask} : set CPU thread affinity mask (hexadecimal number)  -stx{Type} : exclude archive type  -t{Type} : Set type of archive  -u[-][p#][q#][r#][x#][y#][z#][!newArchiveName] : Update options  -v{Size}[b|k|m|g] : Create volumes  -w[{path}] : assign Work directory. Empty path means a temporary directory  -x[r[-|0]][m[-|2]][w[-]]{@listfile|!wildcard} : eXclude filenames  -y : assume Yes on all queriesrun_micro: path=real_exes/7za.execpu_backend: jitentry=0x00000000004e9ac0 initial_rsp=0x000000002000eff8events=243 termination=ExitProcess { code: 0 }
run_micro: ok exit=0bottle_root: /var/folders/16/y3g8vpb14db0rg6g632rl1_m0000gn/T//wie-7za-bottle-71495guest_args: ["a", "-mmt2", "-bd", "C:\\App\\blob.7z", "C:\\App\\blob.bin"]7-Zip (a) 26.02 (x64) : Copyright (c) 1999-2026 Igor Pavlov : 2026-06-25Scanning the drive:1 file, 262144 bytes (256 KiB)Creating archive: C:\App\blob.7zAdd new data to archive: 1 file, 262144 bytes (256 KiB)Files read from disk: 1Archive size: 479 bytes (1 KiB)Everything is Okrun_micro: path=real_exes/7za.execpu_backend: jitentry=0x00000000004e9ac0 initial_rsp=0x000000002000eff8events=3323 termination=ExitProcess { code: 0 }

Будущее проекта и о багах

Как вы знаете, в любом крупном проекте всегда есть баги: явные, скрытые и архитектурные. А при активном использовании нейросетей шанс поймать неочевидную проблему возрастает в разы. Я уверен, что по мере дальнейшей разработки и увеличения покрытия WinAPI обязательно найдутся новые скрытые проблемы в цепочках JIT или синхронизации потоков, но тем интереснее будет их искать и исправлять.

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

В заключение хочу добавить немного личного контекста. Мне 15 лет, и WIE — это мой сугубо исследовательский pet‑проект. Я создал его для того, чтобы глубоко, «руками» прочувствовать системное программирование, разобраться в устройстве JIT‑компиляторов, Cranelift, ассемблере и работе операционных систем с памятью. Согласен, что называть такую разработку нормальной и полноценно обучающей, но по крайней мере мне интересно.

Буду рад любой конструктивной критике архитектуры, советам от старших коллег по оптимизации хелперов памяти и, конечно же, пулл‑реквестам!

Ссылка на репозиторий GitHub: Vladislav‑Kalinkin/wie

Спасибо за внимание! Жду вас в комментариях.

ссылка на оригинал статьи https://habr.com/ru/articles/1061806/