Пролог
В прошлой статье я рассказывал про WIE — userspace-эмулятор, позволяющий через JIT запускать Windows PE64 бинарники на Apple Silicon. Проект развивался отлично, но в какой-то момент я уперся в непреодолимую стену — критическое падение производительности.
Реальный тест на сжатие проекта объемом 200+ МБ утилитой 7-Zip показал ужасающий результат: эмуляция работала в 80–100 раз медленнее, чем нативный macOS бинарник. Исправить этот провал без радикальной перестройки архитектуры и разрушения самой концепции WIE было невозможно. Разработку пришлось приостановить.
Но идея переносить софт между ОС без тяжелой эмуляции не давала мне покоя. Я начал искать решение, которое позволило бы избавиться от JIT и запускать код нативно.
И я его нашел. Имя ему Kakehashi.
Darling: забытый «брат Wine» и его архитектурный тупик
Когда заходит речь о запуске софта для macOS на Linux, первым всегда вспоминают Darling. Это амбициозный проект, который пытается стать для экосистемы Apple тем же, чем Wine стал для Windows. На бумаге концепция выглядит монументально: воссоздать окружение Darwin, транслируя вызовы Mach-ядра в системные вызовы Linux.
Но почему за годы разработки Darling так и не стал стандартом для DevOps-пайплайнов и CI/CD? Дело в фундаментальном архитектурном выборе, который завел проект в тупик.
Ловушка пространства ядра (Kernel Space)
Чтобы сэмулировать специфичное поведение Mach-ядра, потоков, IPC и Mach-портов, Darling использует кастомный модуль ядра Linux (LKM). Это решение мгновенно ставит крест на использовании утилиты в современной серверной автоматизации:
-
Docker-несовместимость: Из соображений безопасности ни один облачный провайдер не разрешит загрузить кастомный модуль ядра хоста из контейнера.
-
Угроза безопасности: Любая уязвимость в модуле ядра Darling — это мгновенный Root-доступ к хост-машине.
-
Ад поддержки: При каждом обновлении ядра Linux модуль норовит сломаться, требуя постоянной пересборки.
Архитектурный и технологический долг
Мало проблем с пространством ядра — Darling исторически создавался под x86_64 архитектуру старых Mac. Проект до сих пор не смог полноценно перестроиться под современную реальность Apple Silicon (ARM64).
Чтобы сэмулировать экосистему macOS, авторы пытаются воспроизвести фреймворки Apple «в лоб». Внутрь затянули огромное количество тяжелых сторонних зависимостей: от open-source кусков кода Apple до проектов GNUStep и Cocotron (древних попыток воссоздать графический стек Cocoa API).
В результате Darling превратился в монолитного монстра, запертого на bare-metal машинах с полными root-правами. Он оказался слишком неповоротливым для облаков.
Философия Kakehashi: Чистый Userspace и Lazy Stubbing
Kakehashi выбирает принципиально другой путь — Wine-way на максимальной скорости:
-
Полный отказ от модулей ядра: Мы не эмулируем Mach-ядро на уровне LKM.
-
Свободная лицензия: Легковесный userspace-транслятор распространяется под лицензией Apache 2.0 (никакого GPL от Darling).
-
Никакого графического мусора: Мы не тащим внутрь GNUStep или Cocotron. Вместо эмуляции тяжелых
.frameworkизнутри, Kakehashi использует тактику Flat Fallback и Lazy Catch-All. Вызовы к закрытым библиотекам Apple перехватываются на самом верхнем уровне ABI и заворачиваются в компактные Rust/C-мосты (stubs).
Кастомный загрузчик kh-loader парсит Mach-O заголовки, мапит секции бинарника в память Linux один к одному (identity-mapped guest VA), подсовывает ему кастомную libSystem.B.dylib и передает управление на точку входа LC_MAIN.
Гостевой код выполняется нативно на процессоре ARM64. Когда программа пытается выполнить системный вызов BSD, наша библиотека перенаправляет его через быстрый ассемблерный мост на функцию khbsd_hypercall, которая переключает контекст прямо в хостовый kh-runtime на Rust. [1]
Никакого root-доступа. Никаких ядерных модулей. Kakehashi изначально проектируется как легковесная CLI-утилита, которая запустится на любой стандартной Ubuntu и внутри изолированного Docker-контейнера.
Как устроен рантайм
Весь процесс запуска macOS-приложения на Linux разбит на четыре изолированных компонента (крейта): kh-cli(интерфейс командной строки), kh-loader (загрузчик бинарников), kh-runtime (обработка сисколлов и логика памяти) и kh-libsystem (наша гостевая библиотека).
Загрузка Mach-O и Identity-Mapping памяти
Поскольку в Linux нет встроенного загрузчика для формата macOS, за дело берется крейт kh-loader. Он парсит заголовки Mach-O (как чистые thin arm64, так и fat-контейнеры), поднимает базовые сегменты (__TEXT, DATA, LINKEDIT), разбирает зависимости вроде LC_LOAD_DYLIB и строит план отображения сегментов в памяти.
Виртуальная память гостя проецируется на адресное пространство хоста строго один в один: guest VA == host VA. Благодаря этому гостевой указатель на буфер является валидным указателем для хост-системы. Сегменты размещаются через обычный mmap хоста с флагами MAP_FIXED. Главный плюс такого подхода — полное отсутствие расходов на копирование данных «гость \(\leftrightarrow \) хост» на каждый байт, что критично для сетевого трафика или сжатия тяжелых архивов.
Freestanding libSystem.B.dylib
Вместо системных библиотек Apple гостю подсовывается изолированная clean-room dylib-библиотека. Она написана на Rust с небольшими C-вставками для функций с переменным числом аргументов (вроде printf / snprintf).
Это не полный ABI настоящей libSystem, а необходимый минимум под конкретный софт (7zz, curl, Git). Если гость вызывает то, чего ещё нет, он натыкается на заглушку (named missing trampoline) — первый реальный вызов логируется в рантайме, а не «тихо врет». Вот пример малой части из того, что уже реализовано:
-
Файловый POSIX:
read,write,open,close— обёртки над Darwin-номерами сисколлов, которые переводит рантайм. -
Собственный Heap: Арена на 64 МиБ + анонимный
mmapдля крупных блоков. -
Потоки: Модель 1:1, где гостевой pthread мапится на реальный поток Linux-хоста через
bsdthread_createи схему синхронизацииpark/wake(футексы). -
Сеть и DNS:
getaddrinfoперенаправляется на host-helper, который делает DNS-запросы через сетевой API хоста.
Мост Hypercall и трансляция сисколлов
В macOS системные вызовы выполняются через инструкцию процессора svc #0x80. Но Linux её не понимает. На основном производственном пути Kakehashi использует механизм Hypercall: загрузчик прошивает слот функции _kh_bsd_hypercall адресом ассемблерной заглушки kh_hypercall_entry. Вызовы идут через быстрый прямой прыжок bl в пользовательский рантайм.
Внутри kh_hypercall_entry происходит следующая последовательность действий:
-
Восстановление хост-TLS: Восстанавливается хостовый регистр
TPIDR_EL0. Гость держит там свой TLS и___error, но хост-код Rust и glibc с гостевым указателем жить не могут. -
Переключение стека: Поток переходит на изолированный
host alt stack(512 КиБ на поток), чтобы не замусорить стек гостя. -
Сохранение NEON: Полностью сохраняются и восстанавливаются регистры процессора
Q0–Q31и регистры состоянийFPCR/FPSR. Воркеры сжатия 7-Zip активно используют векторные инструкции, и малейшая потеря данных здесь рушит многопоточность. -
Диспетчеризация: Вызывается Rust-обработчик, который переводит Darwin BSD-номер сисколла в эквивалент Linux и возвращает результат.
Для отладки оставлен и запасной путь: если гость бьет «голый» svc, рантайм патчит его в инструкцию brk, перехватывает через SIGTRAP ядра Linux и обрабатывает вызов.
Ключевые технические решения
Изолированное дерево ФС (Bottle)
Чтобы гостевые бинарники не пытались ломать системные каталоги Linux, Kakehashi собирает «бутылку» (bottle) — каталог с привычной для macOS раскладкой (/usr, /bin, /private/etc, /Volumes/linux). Абсолютные гостевые пути режутся внутрь этой директории.
Путь /Volumes/linux — это мост к реальной ФС хоста. Так можно не трогать системные каталоги Linux, класть freestanding libSystem.B.dylib в гостевой /usr/lib и ставить утилиты вроде 7zz в /usr/local/bin. CA-сертификаты для HTTPS при вызове kh bottle ensure не вендорятся в git: берется host trust store хоста, либо скачивается актуальный Mozilla-бандл с curl.se.
Потоки 1:1 и корректный join
Каждый гостевой pthread_create порождает реальный host-поток. Чтобы избежать состояния гонки при очистке памяти (когда вызывающий поток pthread_join делает munmap стека до того, как завершающийся поток успел безопасно выйти), используется строгий протокол:
-
Гостевой поток пишет результат в структуру потока и вызывает сисколл
bsdthread_terminate. -
Рантайм уходит в hypercall и переключается на стек хоста.
-
И только находясь на хост-стеке, рантайм выставляет флаг
done = 1и будит джойнеров черезfutex-wake. -
Только после этого
pthread_joinможет безопасно освобождать гостевой стек.
TLS без Rust-макроса thread_local!
Гость использует аппаратный регистр TPIDR_EL0 как базу для GuestTls. На входе в hypercall нужен быстрый возврат host-значения регистра.
Использовать стандартный макрос thread_local! из стандартной библиотеки Rust здесь нельзя: он сам под капотом неявно завязан на регистр TPIDR_EL0 хоста. В момент, когда там находится гостевой TLS, вызов макроса пытается прочитать данные по гостевым смещениям и роняет процесс в SIGSEGV.
Поэтому host TPIDR_EL0 и альтернативный стек (alt_top) зеркалируются прямо внутри структуры гостевого TLS (offset 24 — host_tpidr, offset 32 — alt_top). Горячий путь hypercall читает эти зеркала прямыми ассемблерными инструкциями, не обращаясь к системным вызовам хоста вроде gettid на каждый сисколл.
Автономная распаковка Xcode Command Line Tools
Чтобы запустить Apple Git, его нужно сначала где-то взять. Тащить проприетарные тяжелые блобы в Git-репозиторий проекта — плохая идея. Использовать внешние утилиты вроде p7zip для ковыряния системных архивов Apple внутри контейнеров — ненадежно.
Поэтому был реализован реализовал полностью автономный, чистый конвейер развертывания окружения по команде kh install xcode-tools:
-
Парсинг каталогов Apple: Утилита идет на публичный swcdn.apple.com (откуда качает обновления сама macOS), парсит
.sucatalogи находит актуальный стабильный пакет Command Line Tools. Никаких Apple ID, авторизаций и cookies. -
Скачивание: Оригинальный
.pkgскачивается напрямую с серверов Apple в кэш хоста. -
Собственный конвейер распаковки: В рантайм встроен чистый разборщик цепочки архивов Apple «на лету» без привлечения стороннего софта. Мы берем архив, разбираем формат XAR, распаковываем поток pbzx, прокидываем его через odc cpio и аккуратно раскладываем готовое дерево CLT (
usr/bin/gitи зависимости) прямо внутрь нашей изолированной бутылки.
О производительности и ограничениях
Несмотря на заметный выигрыш по сравнению со старой эмуляцией WIE, околонативного времени выполнения (wall-clock time) на сценариях с большим количеством системных вызовов здесь нет. Постоянное пересечение границы гость-хост дает о себе знать: налог на вызов накладывается на общее число сисколлов.
Для примера я взял дерево из примерно 8 тысяч мелких файлов общим объемом около 240 МиБ и прогнал тест сжатия (Ubuntu aarch64, команда 7zz -t7z -m0=lzma2 -mx=5 -mmt=4):
|
Окружение |
Время выполнения |
Отношение |
|
Нативный Linux 7zz |
~22.5 сек |
~×1.0 |
|
Darwin 7zz под Kakehashi (kh) |
~118 сек |
~×5.2 |
При этом на задачах класса «мало файлов, много чистых вычислений» разрыв падает и держится в районе ×1.1–1.2. Однако для реальной экономики CI/CD-пайплайнов идеальный паритет с macOS-раннером и не требуется. Главное — это сама возможность стабильно гонять Darwin CLI на дешевом Linux arm64.
Сознательные рамки проекта на данный момент:
-
Только архитектура aarch64: Поддержка устаревшего x86_64 не является целью.
-
Минимум графики и подписей: GUI, утилиты
codesignи интерфейс Xcode UI вынесены за рамки приоритетов. -
Curl как сетевой мост: Гостевой
curlиспользовался как сетевой шлюз для обкатки сокетов, а не для достижения полного паритета по флагам с macOS-версией. -
Размер страниц памяти: В дизайн рантайма изначально заложена поддержка страниц как 4 КиБ (стандартные Linux-контейнеры), так и 16 КиБ (системы класса Asahi Linux). Основным полигоном для тестирования остаются Linux aarch64, Docker и виртуалки UTM.
PS: 16 КиБ страницы еще не протестированы, поэтому гарантировать правильную работу не могу
Что уже работает
На текущий момент рантайм стабильно проходит тесты внутри Docker (Colima) и на чистом железе в виртуальных машинах UTM (Ubuntu Server aarch64):
-
Apple Git (из Command Line Tools) — наш главный текущий триумф. Базовые команды контроля версий (
git init,git add,git commit) предварительно работают. Проект честно держит многопроцессныйfork, транслирует абсолютные пути и мапит вызовыgetuid/getcwd, обходя проверки безопасностиsafe.directory. Абсолютная надежность на всех сценариях еще доказывается, но ядро системы уже стабильно. -
Нативный
curl— предварительно полностью закрытый сетевой milestone. На Docker-инфраструктуре скриптdocker-curl-options.shуспешно прогоняет матрицу из 200+ команд (10 тиров тестов: от cookies, параллельной загрузки и ретраев до работы с UNIX-сокетами черезAF_UNIX). Рантайм берет на себя non-blocking сокеты, работу сpollи подменяет проверки сертификатовAppleSecTrustна хостовой OpenSSL с пробросом актуального CA-бандла прямо внутрь бутылки. -
7-Zip (
7zz) — наш основной многопоточный бенчмарк. На текущий момент успешно проходят более 95% всех команд и флагов утилиты. Проект стабильно держит жесткий стресс-тест сжатия в 4 потока (-mmt=4 -mx=5), что подтверждает корректность сохранения NEON-регистров процессора и TLS при переключении контекста в гиперколлах.
Как попробовать
Для запуска Kakehashi вам понадобится среда Linux aarch64 (bare-metal, вторая ОС, виртуальная машина и т.п.) либо Docker / Colima на Apple Silicon.
из crates.io
cargo install kakehashikh bottle ensure kh install 7zip kh install curl kh install xcode-tools
Или из git:
git clone https://github.com/wie-project/kakehashi cd kakehashi cargo install --path crates/kh-cli
Развертывание окружения
kh bottle ensure kh install 7zip kh install curl kh install xcode-tools
Быстрый старт через Docker
Если вы находитесь на Mac с Apple Silicon и хотите быстро проверить рантайм в Linux-контейнере, в репозитории подготовлены smoke-скрипты. Они автоматически соберут Docker-образ, развернут бутылку и запустят тесты:
# Например./scripts/docker-curl-options.sh./scripts/docker-7zz.sh
Примечание: Скрипты в директории scripts/ выполняют роль быстрых smoke-обвязок для базовой проверки стабильности, а не прогона полной матрицы системных вызовов macOS. То есть не ждите готовый полноценный образ для тестирования
Текущие результаты и вместо заключения
Если оценивать проект Kakehashi на сегодняшний день, то рантайм уже вышел за рамки простой концепции. Вот что получается по фактам и цифрам:
-
Apple Git: Базовые команды контроля версий (
init,add,commit) предварительно работают на Linux ARM64. Архитектурный скелет для поддержки многопроцессности иforkсобран, хотя абсолютную надежность на всех сценариях еще только предстоит доказать. -
Darwin curl: Скрипт автоматического тестирования
docker-curl-options.shуспешно прогоняет матрицу из 200+ команд (10 тиров тестов: от обработки cookies и параллельной загрузки до ретраев и UNIX-сокетов). -
7-Zip (
7zz): Успешно проходят более 95% всех команд и флагов оригинальной macOS-утилиты, включая тесты многопоточного сжатия. -
Автономия: Рантайм умеет сам ходить на сервера обновлений Apple, забирать пакеты и распаковывать цепочку архивов
XAR -> pbzx -> cpioпрямо внутри контейнера без привлечения внешнего софта хоста.
Эпилог
Как и в прошлый раз, скажу: проект экспериментальный, а код генерируется ИИ. Но повторю важную вещь: если не контролируешь этот процесс напрямую, на выходе легко получить код, который со временем просто превратится в мусор. Просто лежа на диване и нажимая одну кнопку, ничего не добьешься — для тестирования, нахождения нестандартных ситуаций и банальных идей все еще нужен человек.
Не знаю, брошу ли я этот проект через какое-то время, но пока он идет значительно легче и быстрее, чем старый JIT-эмулятор WIE.
Спасибо за внимание! Буду рад фидбеку и конструктивной критике в комментариях.
Ссылки проекта:
-
Исходный код: https://github.com/wie-project/kakehashi
-
Лицензия: Apache-2.0
ссылка на оригинал статьи https://habr.com/ru/articles/1065502/