Перейти к основному содержимому

JIT-компилятор

Большинство ML-компиляторов либо линкуют целый LLVM в бинарник — добавляя сотни мегабайт зависимостей — либо записывают временные файлы на диск и загружают результат через dlopen. Morok не делает ни того, ни другого.

Когда ядру нужно выполниться, Morok передаёт сгенерированный исходный код в clang через stdin, принимает перемещаемый ELF-объект из stdout, парсит его в процессе, копирует машинный код в анонимный mmap, применяет релокации, переключает права страниц на исполнение и вызывает функцию напрямую по указателю. Весь процесс происходит в памяти — никакие временные файлы не затрагивают диск, никакие разделяемые библиотеки не загружаются, и никакой установки LLVM не требуется кроме clang в PATH.

Эта глава описывает работу JIT-загрузчика для CPU. GPU-бэкенды (CUDA, Metal и др.) используют соответствующие API драйверов для компиляции и диспетчеризации и будут документированы отдельно по мере добавления.

Пайплайн

C source / LLVM IR


clang -c (stdin → stdout)


ELF .o bytes (в памяти)


Парсинг секций (object crate)


Анонимный mmap + копирование секций


Применение релокаций (для каждой архитектуры)


mprotect(PROT_READ | PROT_EXEC)


Сброс I-cache (кроме x86_64)


Вызов через libffi

И Clang-бэкенд (исходный код на C через -x c), и LLVM-бэкенд (текст LLVM IR через -x ir) используют общий загрузчик. Единственное отличие — флаг входного языка clang.

:::tip Режим совместимости Для отладки или платформ, где пользовательский ELF-загрузчик не работает, Cargo-фича dlopen-fallback переключает на традиционный пайплайн: clang -shared записывает .so во временную директорию, которая загружается через dlopen. Это медленнее (дисковый I/O + оверхед динамического компоновщика), но более портабельно. :::

Поддерживаемые архитектуры

АрхитектураTarget tripleФлаг компиляцииI-cacheПримечания
x86_64x86_64-none-unknown-elf-march=nativeКогерентныйAMD64, Intel 64
aarch64aarch64-none-unknown-elf-march=native__clear_cacheApple Silicon, Ampere, Graviton
riscv64riscv64-none-unknown-elf-march=rv64gc__clear_cacheRV64I + M + A + F + D + C расширения
loongarch64loongarch64-none-unknown-elf-march=native__clear_cacheLoongson 3A5000+
ppc64lepowerpc64le-none-unknown-elf-mcpu=native__clear_cacheELFv2 ABI, только little-endian

Архитектура определяется автоматически через std::env::consts::ARCH во время выполнения — никаких compile-time feature-флагов не требуется.

Поддержка релокаций

Загрузчик реализует минимальный ELF-релокатор для каждой архитектуры. Он обрабатывает типы релокаций, которые clang -c -O2 фактически генерирует для небольших самодостаточных вычислительных ядер — это не полноценный линкер.

x86_64 — PC-relative (R_X86_64_PC32, PLT32, GOTPCRELX, REX_GOTPCRELX), абсолютные 32/64-бит (R_X86_64_32, 32S, 64).

aarch64 — 26-битные ветвления (CALL26, JUMP26) с автоматической генерацией трамплинов при превышении диапазона ±128 МБ, страничная адресация ADRP (ADR_PREL_PG_HI21), 12-битные смещения страниц с учётом размера доступа (ADD_ABS_LO12_NC, LDST8/16/32/64/128_ABS_LO12_NC).

riscv64 — пары вызовов (CALL, CALL_PLT), PC-relative раздельная адресация с отслеживанием состояния (PCREL_HI20 + PCREL_LO12_I/S), абсолютная (HI20, LO12_I/S), ветвления (BRANCH, JAL), данные (32, 64). Подсказки релаксации компоновщика (RELAX) пропускаются.

loongarch64 — 26-битные ветвления (B26), страничная раздельная адресация (PCALA_HI20, PCALA_LO12), данные (32, 64). Подсказки релаксации компоновщика (RELAX) пропускаются.

ppc64le — 24-битные ветвления (REL24), TOC-relative адресация с поиском символа .TOC. (TOC16_HA, TOC16_LO, TOC16_LO_DS, TOC16, TOC16_HI), PC-relative (REL32), абсолютная (ADDR32, ADDR64).

Флаги компиляции

Загрузчик компилирует с bare-metal target для получения чистых, самодостаточных ELF-объектов без зависимостей от среды выполнения:

ФлагC-бэкендLLVM IR-бэкендНазначение
-cдадаТолько компиляция (без линковки)
-O2дадаУровень оптимизации
-march=nativeдадаИспользовать возможности хост-CPU
-fPICдадаPosition-independent code
-ffreestandingданетНе предполагается hosted-окружение
-fno-math-errnoдадаMath-builtins не устанавливают errno
-fno-stack-protectorдадаБез накладных расходов на stack canary
-nostdlibданетБез стандартной библиотеки
-fno-identданетПодавить секцию .comment
--target=<arch>-none-unknown-elfдадаBare-metal ELF target
-ffixed-x18aarch64 macOS/Winaarch64 macOS/WinРезервирование платформенного регистра
-funroll-loopsнетдаАгрессивное развёртывание циклов
-fvectorizeнетдаВекторизация циклов
-fslp-vectorizeнетдаSLP (straight-line) векторизация

C-бэкенд использует __builtin_* функции (например, __builtin_sqrtf, __builtin_fmaf) вместо #include <math.h>, поэтому -ffreestanding -nostdlib работает без потери математической поддержки — это intrinsics компилятора, которые напрямую преобразуются в аппаратные инструкции.

Разрешение внешних символов

Если clang генерирует вызов внешней функции (редко — большинство математики обрабатывается builtins), загрузчик разрешает его через dlsym(RTLD_DEFAULT, name) во время загрузки. Это покрывает случаи вроде memcpy или специфичных для платформы символов libm, которые clang может сгенерировать вместо инлайнинга.

Трамплины переходов (aarch64)

На aarch64 релокации CALL26/JUMP26 кодируют PC-relative смещение в 26 битах, что даёт диапазон ±128 МБ. На macOS с ASLR анонимная область mmap обычно находится на ~2 ГБ от системных библиотек вроде libm — далеко за пределами этого диапазона.

Когда загрузчик обнаруживает выход CALL26/JUMP26 за пределы диапазона, он генерирует трамплин (veneer) в зарезервированной области в конце mmap:

LDR X16, [PC, #8] // загрузить 64-битный целевой адрес
BR X16 // непрямой переход
.quad <address> // полный 64-битный адрес

Трамплины предварительно подсчитываются (перед выделением mmap) и дедуплицируются — если несколько точек вызова ссылаются на один и тот же внешний символ, они используют общий трамплин.

Платформенный регистр (aarch64)

На macOS и Windows ARM регистр x18 зарезервирован как платформенный. Поскольку мы компилируем с --target=aarch64-none-unknown-elf (bare-metal), компилятор без дополнительных указаний использовал бы x18 как свободный GPR. Флаг -ffixed-x18 предотвращает это, избегая крашей при выполнении JIT-кода в процессе macOS/Windows.

Когерентность кеша инструкций

На x86_64 кеш инструкций и данных когерентен — запись машинного кода в память и переход к нему работает без дополнительных шагов. На всех остальных архитектурах загрузчик вызывает __clear_cache(start, end) после mprotect, чтобы кеш инструкций увидел новый код.