Как JIT-компилятор рассказывает системе о сгенерированном коде: практический разбор

Автор рассказывает о проблеме, которую столкнулась команда MoarVM (виртуальная машина для языка Raku). Когда Windows изменил реализацию функции longjmp в новых версиях MSVC, она начала требовать полное разворачивание стека кадр за кадром с информацией о раскручивании (unwinding data) для каждой функции. Это стало причиной краха, потому что JIT-компилятор MoarVM не предоставлял эту информацию операционной системе. Статья детально разбирает пять основных способов связи JIT-кода с остальной системой: от простого текстового формата до полноценных загружаемых библиотек.
Ключевые факты
- Скомпилированный код должен содержать метаданные: символы функций, номера строк, информацию о раскручивании стека для обработки исключений.
- MoarVM использует longjmp для обработки исключений на уровне C, что требует специальных данных раскручивания стека.
- Существует пять практических подходов: Perf Map (самый простой), RtlAddFunctionTable / libunwind (среднее), GDB jitreader (гибкий), JITDUMP (полнофункциональный), загружаемые библиотеки (универсальный, но сложный).
- Perf Map работает с 2018 года в MoarVM и позволяет видеть имена функций в отчётах профайлера, но не показывает ассемблер.
- GDB jitreader требует написания плагина для самого отладчика и вручную реализованной процедуры разворачивания кадров.
Ред. Один из немногих постов на HN про бетонные боли внутри виртуальных машин язык, заряженный примерами из реального изгиба кода.
Почему это важно
Когда JIT-компилятор создаёт машинный код на лету, этот код существует только в памяти процесса. Внешние инструменты (операционная система, отладчики, профайлеры) не знают, где этот код находится, как его развернуть при исключении, какие строки исходного кода он реализует. Без этой информации невозможно нормально отлаживать JIT-код, профилировать его или даже безопасно обрабатывать исключения. Проблема усугубляется тем, что разные платформы (Windows, Linux, macOS) требуют разные механизмы передачи этих данных.
Ред. Стандарты экосистемы заставляют JIT-разработчиков говорить на языке ОС вместо того, чтобы внутренняя логика была достаточна.
Кому это важно
Разработчикам интерпретаторов и виртуальных машин (особенно авторам Raku, Ruby, Python, Java), которые используют JIT-компиляцию. Также это касается авторов средств профилирования и отладки (GDB, Perf, IDE), которые пытаются понять, что происходит внутри JIT-сгенерированного кода. И конечно, это влияет на пользователей этих систем, которые хотят эффективно отлаживать и оптимизировать код.
Ред. Слои абстракции редко встречаются дружелюбно кто-то из разработчиков всегда платит цену.
Как это применить
Если вы разрабатываете JIT-компилятор, выбор метода зависит от ваших целей и платформ. Perf Map это лучший старт для Linux, если нужна базовая поддержка профилирования без усложнений. Если нужна полная отладка с номерами строк, выбирайте GDB jitreader или JITDUMP. Для кроссплатформности (Windows + Linux + macOS) рассмотрите RtlAddFunctionTable / libunwind, но будьте готовы к платформе-специфичным нюансам. Если ресурсы позволяют, загружаемые библиотеки это универсальный вариант, хотя и требуют работы с форматами ELF / Mach-O / PE и I/O.
Ред. Выбор инструмента редко бывает чистым: обычно это дебаты между простотой и мощью.
Можно ли доверять
Автор (Lizmat) это известный разработчик Raku с многолетним опытом в MoarVM. Статья основана на реальном инциденте: падение функционала на Windows из-за изменения поведения longjmp в MSVC. Код, о котором идёт речь, опубликован в публичных PR проектов MoarVM. Однако сам автор признаёт, что некоторые из рассмотренных подходов он не полностью тестировал лично и рекомендует проверять документацию соответствующих инструментов (GDB, Perf) самостоятельно. Информация актуальна и основана на документации и реальной практике.
Ред. Автор честен про границы личного опыта знак, что остальное можно проверять, не страхуясь.
Риски и подводные камни
Главный риск это выбрать механизм, который работает только на одной платформе. Например, RtlAddFunctionTable работает только на Windows, libunwind на Linux, GDB jitreader требует своего плагина. Второй риск это производительность: загрузка полных ELF-файлов с диска при каждой компиляции может сильно замедлить JIT. Третий это сложность: написание корректного GDB jitreader или полной ELF-структуры требует глубокого знания форматов и ABI, ошибки могут привести к падению отладчика или неправильной отладке. Четвёртый это чем больше метаданных вы предоставляете, тем больше памяти они занимают и тем сложнее управление их жизненным циклом.
Ред. Лучше один инструмент, что работает повсюду, чем пять инструментов, каждый по своей части.