Этого никто и не обещал, тут понятное дело нужно будет создавать "словарь" по адресам и вручную обзывать функции.
Да, статический анализ никогда не сможет идеально восстановить весь код. Но конечная цель — не «восстановить всё», а создать работоспособную программу, что достигается прагматичными подходами. Главное недоразумение в том, что цель рекомпиляции — не перевести код на 100%, а создать работающую программу, используя прагматичные подходы. Статическая рекомпиляция не пытается быть идеальной, она просто достаточно хороша для практического использования.
1. 🎯 Полный «прогон» игры не нужен (но данные — критичны)
Даже при самом идеальном прохождении, путь игрока в коде — лишь малая толика. Ошибка в том, что он приравнивает код к данным. Например, редко используемые диалоги — это данные, а не код. С ними всё просто: рекомпилятор может передать управление в специальный рантайм, который выполнит нужный кусок кода, если до него всё же дойдёт дело. Остаются только «мёртвые» зоны для разработчиков, которые нигде не вызываются, — их отсутствие в рекомпиляции не страшно.
2. 🧩 Главный трюк: ручная разметка вместо магии
Ни один рекомпилятор не работает на магии. И N64Recomp, и PS2Recomp изначально рассчитаны на помощь человека-исследователя. Вы просто даёте инструменту карту:
Файл символов: Он указывает, где в игре находятся функции и глобальные переменные.
Конфигурация (TOML): Здесь вручную прописываются «заглушки» для сложных систем и подсказывается, как перевести код.
Такой гибрид сберегает нервы разработчика и даёт гарантию точности перевода.
3. 🎮 Игры не такие сложные, как кажется
Сложность статической рекомпиляции часто переоценивают из-за параноидального подхода. На деле современные игры (даже для старых консолей) в основном пишутся на высокоуровневых языках (C/C++), а значит, их код стандартизирован, предсказуем и не пытается скрыть свои границы.
Если же какой-то кусок кода упрятан, инженеры не паникуют, а применяют чёткие стратегии:
Заглушки (Stubs) - Заранее, в конфиге, прописывается заглушка, чтобы компиляция не сломалась при её отсутствии в коде. Не нашёлся код аудиодрайвера — система сделает заглушку, а игра просто запустится без звука.
Динамический поиск - Алгоритм эвристически ищет код, опираясь на стандартные шаблоны компилятора (например, определяет начало функции по push ebp или аналогам). Инструмент находит jr $ra и догадывается, что это конец функции.
Гибридный прогон - Игра как бы «запускается» под наблюдением: пишется лог адресов инструкций, которые выполнились, и только они потом переводятся в код. (Вероятно имелся в виду именно такой подход — самый надёжный для очень сложных проектов)
Проблема в том, что в бинарном коде таблица переходов switch и код вокруг неё — это просто поток чисел, и статическому анализатору нужно как-то угадать границы. Но это не значит, что задача невыполнима: рекомпиляторы и дизассемблеры научились распознавать их с помощью следующих методов.
🕵️♂️ Метод 1: Поиск по шаблону (Pattern Matching)
Это самый распространённый подход. Анализатор ищет в коде характерную сигнатуру, которую оставляют компиляторы. В 99% случаев switch генерируется по чёткому сценарию.
Типичный паттерн: Сначала выполняется проверка границ значения переменной. Затем значение преобразуется в индекс. После этого код загружает адрес из таблицы переходов (jump table) по этому индексу и выполняет безусловный переход (jr $t0 на MIPS или jmp [rax] на x86).
Чем это полезно: Обнаружив такую последовательность из инструкций «проверка-вычисление-загрузка-переход», анализатор с высокой уверенностью говорит: «Это switch». Затем легко вычислить и саму таблицу.
Ограничение метода: Этот подход ненадёжен для нестандартного или обфусцированного кода. Например, Majin Buu's Revenge использует switch всего с одним case. Такой код часто генерируется условным переходом, похожим на if, а не на полноценную таблицу, из-за чего автоматические инструменты могут его не распознать.
🔬 Метод 2: Анализ потока данных (Data Flow Analysis)
Этот метод применяется, когда простой поиск по шаблону не срабатывает.
Обратный анализ (Backward Slicing): Вместо того чтобы искать готовый шаблон, анализатор идёт от инструкции jr $t0 (прыжок) назад по ходу выполнения программы. Он выясняет, как регистр t0 получил своё значение. Например, он может обнаружить, что t0 = table_base + index * 4. Если ему удаётся отследить, откуда берётся table_base, то он может восстановить расположение таблицы, даже если код вокруг неё выглядит нестандартно.
Результат позволяет точно определить, где в памяти лежат адреса переходов для каждого case, даже если компилятор их немного завуалировал.
🤝 Метод 3: Гибридный подход (Символическое выполнение)
Это уже «тяжёлая артиллерия», которая используется для очень сложных случаев.
Пример JTR: Исследовательский инструмент JTR решает проблему, вычисляя все возможные значения, которые могут оказаться в регистре перед прыжком. Если удаётся определить, что их, например, 10, значит, перед нами switch с таким количеством case.
Пример PS2Recomp: Анализатор ps2xAnalyzer этого рекомпилятора комбинирует методы. Сначала он пытается «угадать» таблицу переходов, а если не получается — оставляет для неё специальную заглушку (stub). При запуске, если игра доходит до этого места, специальная рантайм-система перехватывает управление, определяет адрес перехода и «доучивает» перевод. Этот подход надёжен и не приводит к сбоям.
полностью автоматическое статическое определение во всех возможных случаях — задача нерешаемая. Но на практике инженеры создают инструменты, которые справляются с этим достаточно хорошо — для 90-95% кода, написанного стандартными компиляторами.
Именно поэтому рекомпиляторы прекрасно работают, когда их создатели либо «обучают» их на тысячах примеров, либо предусматривают механизмы для ручного уточнения и динамической доводки.
Но судя из ответа ии, да, ты от части прав, и полная 100% статическая рекомпиляция невозможна, но близкая к этому вполне. Хотя возможно иишка и наврала, они это умеют, а мои полномочия на этом как-бы все