docs: bring living project documentation under version control
ANALYSIS.md/ARCHITECTURE.md/PROGRESS.md and the rest of this project's living docs have always lived one directory above this repo's root (NFSMW_Online_Claude_workdir/*.md), so they were never actually part of this git history despite being the authoritative record of every hook, offset, and RE finding this branch's code is built on. Mirrors the docs/ layout already used on native-arm32-trace-harness so both branches reference the same file set by name, pending the actual code merge (see PROGRESS.md's own "Repo merge pending" note). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
# Статическая перекомпиляция ARM32 → LLVM IR → ARM64
|
||||
|
||||
**Статус: запасной путь.** Не начинать, пока не исчерпан текущий подход (JIT-трансляция через Unicorn).
|
||||
Документ создан 2026-09-21 по запросу как запись направления, к которому осмысленно вернуться, если
|
||||
скорость движка упрётся в потолок.
|
||||
|
||||
---
|
||||
|
||||
## 1. Когда переключаться на этот путь
|
||||
|
||||
Критерий один и он числовой. Текущий подход даёт **отставание примерно в 2,6 раза** от реального времени.
|
||||
Измеренный потолок его оптимизации:
|
||||
|
||||
| шаг | ожидаемый результат |
|
||||
|---|---|
|
||||
| обход softmmu (задача #61) | ~1,4x → остаётся ~1,8x отставания |
|
||||
| дальнейшие микрооптимизации | в лучшем случае 1,3–1,5x, оптимистично |
|
||||
| **натив** | **недостижим в JIT-подходе** |
|
||||
|
||||
**Переключаться, если:** после обхода softmmu игра всё ещё не держит 30 кадров в гонке, и дальнейшие
|
||||
замеры не показывают крупных резервов.
|
||||
|
||||
**Не переключаться, если:** 30 кадров достигнуты. Цель — играбельность, а не бенчмарк.
|
||||
|
||||
### Цена переключения, которую надо знать заранее
|
||||
|
||||
**Этот путь обнуляет наработки по мультиплееру.** Вся работа по внедрению сетевого кода делалась в
|
||||
расчёте на перехват функций живого ARM32-бинарника через хуки Unicorn. После перекомпиляции бинарника
|
||||
не будет — будет свой нативный код, и точки внедрения придётся искать заново, уже в другом виде
|
||||
(зато, вероятно, удобнее: в статически слинкованном коде можно просто подменить символ).
|
||||
|
||||
Это самая серьёзная цена, и она не техническая, а проектная. Учитывать при решении.
|
||||
|
||||
---
|
||||
|
||||
## 2. Что это за подход и чем он отличается от «отреверсить игру»
|
||||
|
||||
**Это НЕ декомпиляция.** Никто не читает код, не восстанавливает классы, не пишет C++ заново.
|
||||
|
||||
Машинные инструкции ARM32 **механически** переводятся в промежуточное представление LLVM, а затем
|
||||
компилируются в нативный ARM64. Инструмент не понимает, что делает код — он сохраняет его поведение
|
||||
команда за командой.
|
||||
|
||||
Известные работающие примеры этого класса: **N64Recomp** (использован для портов игр с Nintendo 64 на PC),
|
||||
аналогичные проекты для PS2 и GameCube.
|
||||
|
||||
### Почему это даёт скорость, которой не даст JIT
|
||||
|
||||
Ключ не в том, что трансляция происходит заранее. Ключ в том, что после лифтинга код попадает в
|
||||
**настоящий оптимизирующий компилятор**:
|
||||
|
||||
| | JIT (сейчас) | статическая перекомпиляция |
|
||||
|---|---|---|
|
||||
| Флаги процессора ARM32 | пересчитываются после каждой операции, даже если не нужны | LLVM выбросит мёртвые вычисления |
|
||||
| Регистры | 16 гостевых мапятся на 31 хостовый, лишние простаивают | распределение регистров с нуля, все 31 |
|
||||
| Область оптимизации | внутри одного блока трансляции | межпроцедурная, всё приложение |
|
||||
| Инлайнинг, векторизация | нет | стандартные проходы LLVM |
|
||||
|
||||
Именно поэтому это **единственный путь** к «неотличимо от натива». Для ориентира: Rosetta 2 от Apple —
|
||||
заранее скомпилированная трансляция плюс аппаратная поддержка в процессоре — даёт 70–80% нативной
|
||||
скорости.
|
||||
|
||||
---
|
||||
|
||||
## 3. Почему именно наш случай необычно удобен
|
||||
|
||||
Обычные блокеры статической трансляции у нас частично или полностью сняты, и это **измеренные факты**,
|
||||
а не предположения.
|
||||
|
||||
### Границы функций известны
|
||||
|
||||
В бинарнике есть секция `.ARM.exidx` — таблица раскрутки стека для исключений C++:
|
||||
|
||||
```
|
||||
.ARM.exidx 0x97e150 – 0x9aad18 (0x2cbc8 байт)
|
||||
```
|
||||
|
||||
По 8 байт на запись это **≈22 900 записей**, каждая указывает на начало функции. Главная проблема
|
||||
статической трансляции — «где начинается код» — решена самим бинарником. IDA независимо нашла
|
||||
**34 726 функций**, что согласуется.
|
||||
|
||||
### Самомодифицирующегося кода нет
|
||||
|
||||
Проверено счётчиком на уровне Unicorn: за полный прогон загрузки пролога — **ноль записей гостя в
|
||||
`.text`**. Это значит, что переведённый код не нужно инвалидировать и перетранслировать.
|
||||
|
||||
### Код и данные разделены
|
||||
|
||||
`.text` (9,5 МБ) отделён от `.rodata`, `.data`, `.bss`. Не надо угадывать, где инструкции, а где таблицы.
|
||||
|
||||
### Релокации дают карту указателей
|
||||
|
||||
`.rel.dyn` (387 КБ, ~48 000 записей `R_ARM_RELATIVE`) перечисляет все места, где лежат адреса.
|
||||
Это карта того, что является указателем, а что числом.
|
||||
|
||||
### Чужой код, который действительно не надо переводить — 19,3%, а не треть
|
||||
|
||||
**Исходная оценка «треть бинарника чужая» оказалась завышенной примерно вдвое.** Подсчёт по адресным
|
||||
диапазонам, подтверждённый тремя независимыми методами (кластеризация ссылок на строки, минимальный
|
||||
разрез графа вызовов, тип записей `.ARM.exidx`):
|
||||
|
||||
| библиотека | диапазон | функций | байт | vtable внутри |
|
||||
|---|---|---|---|---|
|
||||
| zlib 1.2.11 | `0x660400–0x66a000` | 61 | 39 612 | 0 |
|
||||
| libjpeg | `0x774000–0x790900` | 285 | 114 836 | 0 |
|
||||
| libpng 1.5.10 | `0x790900–0x7ad844` | 394 | 113 808 | 0 |
|
||||
| curl 7.56.0 | `0x7c72c4–0x80e9b0` | 748 | 281 300 | 0 |
|
||||
| OpenSSL 1.1.0f | `0x80e9b0–0x963120` | 5 014 | 1 156 034 | 0 |
|
||||
| **итого** | | **6 502** | **1 705 590** | **0** |
|
||||
|
||||
Границы подтверждены independently: одна запись `.ARM.exidx` с признаком CANTUNWIND покрывает
|
||||
1 687 132 байта одним куском — curl и OpenSSL собраны с `-fno-unwind-tables`, больше ничто в образе так
|
||||
не собрано. Её концы совпадают с локальными минимумами разреза графа вызовов. И **ни одной C++ vtable**
|
||||
внутри этих диапазонов — игровой код туда не затёк.
|
||||
|
||||
### Три ошибки первой редакции этого документа
|
||||
|
||||
| было записано | на самом деле | как проверено |
|
||||
|---|---|---|
|
||||
| FMOD влинкован статически | **Нет.** `DT_NEEDED: libfmodex.so, libfmodevent.so` — отдельные библиотеки | `readelf -d` |
|
||||
| libc++ влинкован статически | **Нет.** `DT_NEEDED: libc++_shared.so`; в образе только заголовочные шаблоны | `readelf -d` |
|
||||
| 2 432 «именованные» функции | Из них 1 170 — автогенерация IDA (`nullsub_*`), почти всё остальное — PLT-заглушки. **Восстановленных внутренних символов практически ноль** | гистограмма префиксов |
|
||||
|
||||
Пропущены были **libjpeg** (опознан по таблице сообщений `jerror.c`) и **Bullet Physics** (210 имён
|
||||
классов `bt*`).
|
||||
|
||||
### Почему boost, EASTL и libc++ заменить НЕЛЬЗЯ
|
||||
|
||||
Первая редакция утверждала, что шаблоны стандартной библиотеки «пересобираются из заголовков, а не
|
||||
переводятся». **В механическом лифтере это не работает.** У лифтера нет исходников. Чтобы не переводить
|
||||
тело `std::vector<Foo>::push_back`, нужно опознать инстанцирование, восстановить точную раскладку `Foo` и
|
||||
доказать совместимость с хостовой версией — а это ручной реверс, ровно то, ради отказа от чего и выбран
|
||||
этот путь.
|
||||
|
||||
То же касается boost (610 имён классов, 792 vtable, 1 247 виртуальных целей) и EASTL: они **размазаны по
|
||||
всему `.text`** и не отделяются по адресам. Переводить как обычный код.
|
||||
|
||||
### Что действительно облегчает задачу
|
||||
|
||||
**Образ целиком в режиме ARM, без Thumb.** 2 249 305 инструкций в 8 989 480 байтах — ровно 4,0 байта на
|
||||
инструкцию. В `.data.rel.ro` 27 107 указателей на ARM-код против 123 с Thumb-битом. Ноль `tbb`/`tbh`,
|
||||
ноль `ldr pc,`, ноль `mov pc,`. Проверено независимо: все 34 экспортируемые функции имеют чётные адреса.
|
||||
**Нет переключения режимов, нет IT-блоков, нет Thumb-таблиц переходов** — заметно более простая цель, чем
|
||||
предполагалось.
|
||||
|
||||
> Побочно: комментарий в `guest_engine.cpp:3901` называет Thumb «единственным реальным режимом этого
|
||||
> движка». Это **неверно** и может ввести в заблуждение. Работе движка не мешает (режим берётся из CPSR),
|
||||
> но как ориентир — ошибка.
|
||||
|
||||
**`.ARM.exidx` покрывает `.text` на 100%** — 22 905 записей размечают 9 526 532 из 9 527 632 байт.
|
||||
|
||||
**Дубликаты.** 4 918 функций побайтово идентичны и сводятся к 739 различным телам: 1 148 × `bx lr`
|
||||
(пустой виртуальный метод), 678 × `mov r0,#0; bx lr`, 322 × переходник `boost::function`. Лифтер,
|
||||
хеширующий тела, выдаёт 739 вместо 4 918 — **4 179 функций бесплатно**.
|
||||
|
||||
**Длинный хвост мелочи.** 10 646 функций (31,6%) короче 32 байт и занимают всего 1,4% кода. При этом
|
||||
1 613 функций (4,8%) длиннее килобайта и занимают 39,6%.
|
||||
|
||||
## 4. Главная нерешённая трудность: косвенные переходы
|
||||
|
||||
`blx <reg>` — вызов по адресу из регистра. Статически неизвестно, куда он ведёт.
|
||||
|
||||
**Замерено по всем 2 249 305 инструкциям образа:**
|
||||
|
||||
| | количество |
|
||||
|---|---|
|
||||
| `blx <reg>` — косвенные вызовы | **36 232** |
|
||||
| `bl` — прямые вызовы | 136 075 |
|
||||
| доля косвенных среди всех вызовов | **21%** |
|
||||
| `bx <reg>` (в основном `bx lr`, возвраты) | 9 364 + 1 833 условных |
|
||||
| `pop`/`ldm` с `pc` (возвраты) | 27 011 |
|
||||
| `ldr pc,` / `tbb` / `tbh` / `mov pc,` | **0** |
|
||||
|
||||
### Сколько целей удаётся собрать статически
|
||||
|
||||
| | количество |
|
||||
|---|---|
|
||||
| vtable, привязанных к typeinfo | **4 001** |
|
||||
| слотов в них | 25 631 (из них 441 чисто виртуальных) |
|
||||
| **различных виртуальных целей** | **10 624** |
|
||||
| все указатели на код в данных (`.data.rel.ro`, `.got`, `.data`, `.init_array`) | 28 003 → **12 409 различных целей** |
|
||||
| **покрытие функций `.text`** | **36,7%** |
|
||||
|
||||
### И вот здесь главная оговорка, которой не было в первой редакции
|
||||
|
||||
Первая редакция утверждала, что «большинство виртуальных целей можно собрать статически». Для **vtable**
|
||||
это верно. Для **колбэков — нет.**
|
||||
|
||||
В этой сборке с позиционно-независимым кодом взятие адреса функции в регистр выглядит как
|
||||
`ldr rX,[pc,#N]; add rX,pc` — литерал хранит **смещение относительно PC** и **не требует релокации**
|
||||
(проверено на дизассемблере по адресу `0x7c758`). Значит таблица релокаций такие цели **не видит**.
|
||||
|
||||
Мера того, насколько она их не видит: **8 818 функций (1 405 236 байт) не имеют ни одного входящего
|
||||
прямого вызова, ни одного указателя из данных.** Часть — мёртвый код, оставленный компоновщиком.
|
||||
Остальное — колбэки, достижимые только анализом литеральных пулов.
|
||||
|
||||
**Отсюда следует порядок работ:** первым делом нужен не транслятор, а **сканер литеральных пулов**.
|
||||
Если покрытие косвенных целей не удастся поднять существенно выше 37%, то запасной путь через
|
||||
хеш-таблицу «адрес → функция» съест ровно тот выигрыш в скорости, ради которого всё затевается.
|
||||
|
||||
## 5. Что ещё придётся решить
|
||||
|
||||
| задача | сложность | комментарий |
|
||||
|---|---|---|
|
||||
| **Исключения C++** | высокая | Есть `.ARM.exidx`/`.ARM.extab`. Раскрутка стека ARM32 не переносится на ARM64 напрямую — нужна либо своя реализация, либо отображение на нативные исключения |
|
||||
| **Модель памяти** | средняя | ARM32 и ARM64 имеют разные гарантии упорядочивания. При многопоточности возможны тонкие гонки, которых не было на оригинале |
|
||||
| **Точность флагов** | средняя | Где флаги реально читаются — надо сохранить. LLVM выбросит лишнее только если правильно разметить |
|
||||
| **JNI-граница** | низкая | Уже решена в текущем движке, переносится почти как есть |
|
||||
| **Системные вызовы и libc** | низкая | Уже есть полный набор шимов, линкуется напрямую |
|
||||
|
||||
---
|
||||
|
||||
## 6. Первый шаг, если решим начать
|
||||
|
||||
**Не писать транслятор.** Порядок такой:
|
||||
|
||||
1. **Сканер литеральных пулов** (см. раздел 4). Это главный риск всего направления, и он проверяется
|
||||
раньше всего. Цель — поднять покрытие косвенных целей существенно выше 37%. Если не выходит —
|
||||
направление не окупается, и лучше узнать это на первом шаге.
|
||||
2. **Покрытие кода.** Включить блочный профилировщик (`EnableProfiling()`, уже есть в движке) и записать
|
||||
исполненные адреса за полный сеанс: загрузка, меню, гонка, финиш. Это покажет, сколько из 23 000 тел
|
||||
реально работает, а сколько — мёртвый код.
|
||||
3. **Прототип на одной функции.** Перевести одну чистую вычислительную функцию через LLVM IR,
|
||||
подставить в работающий движок вместо эмулируемой и **замерить**. Это даст реальный коэффициент
|
||||
ускорения — единственную цифру в этом документе, которая будет фактом, а не оценкой.
|
||||
|
||||
---
|
||||
|
||||
## 7. Объём работ — итоговая таблица
|
||||
|
||||
| категория | функций | доля | что делать |
|
||||
|---|---|---|---|
|
||||
| zlib, libjpeg, libpng, curl, OpenSSL | 6 502 | 19,3% | **заменить линковкой** — непрерывные диапазоны, ноль vtable |
|
||||
| Bullet Physics | 1 828 | 5,4% | переводить (замена — отдельное исследование) |
|
||||
| фреймворки EA, однозначные | 3 784 | 11,2% | переводить; обёртка GLES — единственный кандидат на замену |
|
||||
| смешанные области EA и игры | 3 798 | 11,3% | переводить, по адресам не разделяются |
|
||||
| **игра и движок** | **17 783** | **52,8%** | **переводить — неустранимое ядро** |
|
||||
|
||||
Всего реальных функций в `.text`: **33 695** (прежние 34 726 включали 516 заглушек PLT и 515
|
||||
плейсхолдеров импорта, которые кодом не являются).
|
||||
|
||||
**Требуют механического перевода: 27 193.** После дедупликации по содержимому — около **23 000 различных
|
||||
тел, ~1,82 млн инструкций ARM**.
|
||||
|
||||
---
|
||||
|
||||
## 8. Честный вывод
|
||||
|
||||
Направление **выполнимо** в том смысле, что 23 000 тел — работа для машины, а не для человека. Условия
|
||||
лучше, чем казалось: режим только ARM без переключений, `.ARM.exidx` покрывает код на 100%, **все 2 310
|
||||
имён классов RTTI сохранились целиком**, 4 001 vtable дают 10 624 разрешённых виртуальных цели, а треть
|
||||
функций короче 32 байт.
|
||||
|
||||
Но **главный риск не в объёме, а в 36 232 косвенных вызовах**, чьи цели собираются лишь частично.
|
||||
Начинать надо с проверки именно этого, а не с транслятора.
|
||||
|
||||
И цена из раздела 1 остаётся в силе: **этот путь обнуляет наработки по мультиплееру.**
|
||||
Reference in New Issue
Block a user