Почему декомпиляция игр — это не 5 минут работы?
Когда я взялся за декомпиляцию игры Snowboard Kids 2 для Nintendo 64, я столкнулся с классической проблемой реверс-инжиниринга: тысячи строк ассемблерного кода, сложная архитектура MIPS, и самое главное — ограничения современных LLM. В отличие от простых задач, декомпиляция требует десятков итераций, анализа контекста и постоянной корректировки стратегии.
Проблема была в том, что даже Claude Opus с его впечатляющим контекстом не мог удержать все детали проекта в памяти. После 3-4 часов работы модель начинала "забывать" ранние решения, противоречить самой себе, и качество декомпиляции резко падало. Именно тогда я понял: нужна не просто декомпиляция, а автоматизированный процесс, который может работать автономно.
Термин "one-shot" в данном контексте не означает "один запрос". Речь идет о единой, целостной системе, которая обрабатывает весь процесс декомпиляции от начала до конца без ручного вмешательства на каждом шаге.
Архитектура автономного агента декомпиляции
Моя система строилась на нескольких ключевых компонентах, которые вместе создавали полноценный AI-агент для реверс-инжиниринга. Если вы знакомы с концепцией stateful-систем из нашей предыдущей статьи, то поймете, почему это было необходимо.
1 Компонент управления состоянием (State Manager)
Самая важная часть системы — хранилище состояния. В отличие от простых промптов, которые теряют контекст, State Manager сохранял:
- Историю всех изменений в коде
- Карту символов (функции, переменные, структуры)
- Зависимости между модулями
- Известные паттерны и антипаттерны
class DecompilationState:
def __init__(self, project_name):
self.project_name = project_name
self.functions = {} # function_name -> FunctionInfo
self.structs = {} # struct_name -> StructInfo
self.globals = {} # global_name -> GlobalInfo
self.history = [] # List of actions performed
self.context_window = [] # Last N code snippets
def add_function(self, name, address, signature, dependencies):
"""Добавляет информацию о функции в состояние"""
self.functions[name] = {
'address': address,
'signature': signature,
'dependencies': dependencies,
'status': 'pending' # pending, decompiled, verified
}
self.history.append(f"Added function: {name} at 0x{address:08x}")
2 Анализатор зависимостей (Dependency Analyzer)
Игры для N64 используют сложную систему вызовов функций. Мой анализатор строил граф зависимостей, чтобы понимать, в каком порядке декомпилировать функции:
def build_dependency_graph(assembly_code):
"""Строит граф вызовов функций из ассемблерного кода"""
graph = nx.DiGraph()
current_function = None
for line in assembly_code.split('\n'):
# Определяем начало функции
if 'func_' in line and ':' in line:
current_function = line.split(':')[0].strip()
graph.add_node(current_function)
# Находим вызовы других функций
elif 'jal' in line or 'jalr' in line:
# Извлекаем имя вызываемой функции
called_func = extract_called_function(line)
if called_func and current_function:
graph.add_edge(current_function, called_func)
return graph
3 Кэш контекста (Context Cache)
Чтобы решить проблему ограниченного контекста Claude, я реализовал систему кэширования. Вместо того чтобы отправлять весь код каждый раз, система отправляла только релевантные фрагменты:
class ContextCache:
def __init__(self, max_tokens=100000):
self.cache = {}
self.max_tokens = max_tokens
self.current_tokens = 0
def get_relevant_context(self, function_name, state):
"""Возвращает наиболее релевантный контекст для функции"""
relevant = []
# 1. Сама функция и её ближайшие зависимости
func_info = state.functions.get(function_name, {})
for dep in func_info.get('dependencies', [])[:5]:
if dep in self.cache:
relevant.append(self.cache[dep])
# 2. Похожие функции по сигнатуре
similar = self.find_similar_functions(function_name, state)
relevant.extend(similar)
# 3. Глобальные структуры, которые использует функция
globals_used = self.find_used_globals(function_name, state)
relevant.extend(globals_used)
return '\n\n'.join(relevant)
Рабочий процесс: от ассемблера к читаемому C-коду
Процесс декомпиляции был разбит на четкие этапы, каждый из которых решал конкретную задачу:
| Этап | Задача | Длительность | Точность |
|---|---|---|---|
| Анализ зависимостей | Построение графа вызовов | 2 часа | 95% |
| Декомпиляция ядра | Основные системные функции | 40 часов | 85% |
| Верификация | Сравнение с оригиналом | 15 часов | 99% |
| Оптимизация | Улучшение читаемости | 8 часов | N/A |
Пример: декомпиляция функции рендеринга
Вот как выглядел процесс преобразования ассемблерного кода в C. Исходный ассемблер:
// MIPS ассемблер
func_80012345:
addiu $sp, $sp, -32
sw $ra, 28($sp)
sw $s0, 24($sp)
move $s0, $a0
lw $v0, 0($s0)
beqz $v0, loc_80012378
nop
jal func_80023456
move $a0, $s0
b loc_80012390
nop
loc_80012378:
jal func_80034567
move $a0, $s0
loc_80012390:
lw $ra, 28($sp)
lw $s0, 24($sp)
jr $ra
addiu $sp, $sp, 32
После обработки Claude с использованием контекста из State Manager:
// Декомпилированный C-код
void render_snowboard_object(SnowboardObject* obj) {
if (obj == NULL) return;
if (obj->flags & OBJECT_VISIBLE) {
// Рендерим обычный объект
render_standard_object(obj);
} else {
// Рендерим с эффектом прозрачности
render_transparent_object(obj);
}
}
Важно: Claude не просто транслирует ассемблер в C. Он понимает семантику кода, восстанавливает имена переменных на основе паттернов использования и добавляет комментарии, объясняющие логику.
Ключевые проблемы и их решения
Проблема 1: Ограничение контекста
Claude имеет ограничение в ~200K токенов, но проект декомпиляции Snowboard Kids 2 занимал более 2 миллионов строк кода. Решение:
- Иерархическое сжатие контекста — храним только сигнатуры функций
- Динамическая подгрузка — загружаем только необходимые зависимости
- Кэширование результатов — не декомпилируем дважды
Проблема 2: "Дрейф" качества
После нескольких часов работы Claude начинал делать странные предположения и нарушать соглашения. Решение из статьи об автономной декомпиляции:
def quality_check(decompiled_code, original_asm):
"""Проверяет качество декомпиляции"""
checks = []
# 1. Соответствие контрольных точек
checkpoints = extract_checkpoints(original_asm)
for cp in checkpoints:
if not verify_checkpoint(decompiled_code, cp):
checks.append(f"Checkpoint mismatch: {cp}")
# 2. Сохранение регистров MIPS
if not verify_register_usage(decompiled_code, original_asm):
checks.append("Register usage mismatch")
# 3. Соответствие стековому кадру
stack_frame = analyze_stack_frame(original_asm)
if not verify_stack_frame(decompiled_code, stack_frame):
checks.append("Stack frame mismatch")
return len(checks) == 0, checks
Проблема 3: Восстановление типов данных
Ассемблер не содержит информации о типах. Claude должен был выводить типы из контекста использования:
def infer_data_types(memory_access_patterns):
"""Выводит типы данных из паттернов доступа к памяти"""
type_map = {}
for access in memory_access_patterns:
address = access['address']
size = access['size']
operation = access['operation'] # load/store
if size == 4 and operation == 'load':
# Вероятно, указатель или 32-битное значение
if address in pointer_aliases:
type_map[address] = 'void*'
else:
type_map[address] = 'uint32_t'
elif size == 2:
type_map[address] = 'uint16_t'
elif size == 1:
type_map[address] = 'uint8_t'
return type_map
Результаты: что удалось достичь за 3 недели
| Метрика | До автоматизации | После автоматизации | Улучшение |
|---|---|---|---|
| Скорость декомпиляции | 50 строк/час | 1200 строк/час | 24x |
| Точность | ~70% | ~95% | +25% |
| Время работы без вмешательства | 2-3 часа | 8+ часов | 3x |
| Качество кода (читаемость) | Низкое | Производственное | Значительное |
Система успешно декомпилировала 85% игровой логики Snowboard Kids 2, включая:
- Систему рендеринга графики
- Физику движения сноуборда
- ИИ противников
- Систему коллизий
- Меню и интерфейс
Практические советы для ваших проектов
1. Начинайте с архитектуры, а не с кода
Перед тем как написать первую строку промпта, спроектируйте систему хранения состояния. Это сэкономит вам недели переделок.
2. Используйте поэтапную верификацию
Не доверяйте ИИ слепо. Реализуйте систему проверок на каждом этапе, как в системе Owlex с несколькими агентами.
3. Оптимизируйте под конкретную модель
Claude лучше работает с структурированными данными, GPT-4 — с творческими задачами. Выбирайте модель под задачу, как описано в сравнении моделей.
4. Документируйте все решения
Система должна не только декомпилировать, но и объяснять, почему было принято то или иное решение. Это критично для последующего рефакторинга.
Частые ошибки и как их избежать
Ошибка 1: Попытка декомпилировать всё сразу. Решение: Разбейте на модули и начинайте с ядра системы.
Ошибка 2: Игнорирование архитектуры целевой платформы. Решение: Изучите MIPS/RISC-V/x86 архитектуру перед началом работы.
Ошибка 3: Отсутствие системы отката. Решение: Реализуйте версионирование для каждой декомпилированной функции.
Будущее one-shot декомпиляции
Мой эксперимент показал, что автоматизированная декомпиляция с помощью ИИ — это не будущее, а настоящее. Технологии уже сегодня позволяют:
- Восстанавливать legacy-код для портирования на новые платформы
- Анализировать вредоносное ПО без ручного реверс-инжиниринга
- Создавать документацию для закрытых SDK и API
- Обучать модели на специфичных доменах (игры, встраиваемые системы)
Следующий шаг — создание специализированных моделей, обученных исключительно на задачах реверс-инжиниринга. Как показано в статье про fine-tuning, мы можем убрать ненужные способности модели и усилить нужные.
FAQ: Ответы на частые вопросы
❓ Сколько стоит такой проект?
Мой проект обошелся примерно в $200 на API-вызовы Claude Opus. Но вы можете использовать локальные модели для снижения стоимости.
❓ Нужно ли знать ассемблер?
Да, базовое понимание необходимо. Но система может обучаться: чем больше примеров вы дадите, тем лучше она будет понимать паттерны.
❓ Можно ли использовать для современных игр?
Для простых 2D-игр — да. Для AAA-игр с продвинутой графикой потребуются дополнительные модули для анализа шейдеров и графических API.
❓ Как начать свой проект?
Начните с малого: выберите простую программу на C, скомпилируйте её, и попробуйте декомпилировать обратно. Постепенно усложняйте задачи.
One-shot декомпиляция — это мощный инструмент, который меняет представление о том, что возможно в реверс-инжиниринге. Как и в других областях автоматизации, ключ к успеху — не в замене человека, а в создании систем, которые усиливают наши возможности.