Здравствуйте. Меня зовут Владислав, и я хочу рассказать о своем экспериментальном проекте на ранней стадии. Называется он 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/