Comments 5
Ага. Интересно.
Можно пример декомпиляции кода на VB6 с вызовом внешних функций из С библиотек. С войной с соглашениями вызовов и угадыванием сигнатур?
Перевод в промежуточное представление машинных кодов хоть и обеспечивает универсальность для дальнейшей декомпиляции имеет и свои недостатки, такие как потеря той информации, которая идет в порядке инструкций и выборе машинных инструкций. Декомпиляторов имеющих возможности посмотреть из верхнего уровня на уровень инструкций за подсказками я не знаю.
Использование LLVM для целей декомпиляции же я считаю вообще ошибкой, эта штука проектировалась для ОПТИМИЗАЦИИ, а в этой задаче нужна ДЕОПТИМИЗАЦИЯ - для человекочитаемости. Нужно распутивать граф управления в том числе дублированием блоков кода (это насколько знаю умеет только fernflower), приводить switch case к нормальному виду из мешанины if и прочее.
Ни один из декомпиляторов нормально не поддерживает объектный и шаблонный код на С++, результат - месиво просто.
Тут совсем так немножечко решил написать свой декомпилятор и дизассемблер:пока только для x16-x32 - "заболел" ретро играми (DOS, Windows, Atari)
Часть из моего ТЗ
10.1. Функциональные критерии
Корректное декодирование инструкций x86 (16/32 бита) на реальных бинарных примерах, включая все addressing modes, префиксы, переходы, вызовы, арифметику, работу с памятью, стеком, портами, флагами, прерываниями.
Соответствие формата вывода выбранному синтаксису (по умолчанию — Intel/MASM/TD), поддержка всех вариантов записи операндов, префиксов, сегментов, смещений, литералов.
Корректная работа виртуализации (отображение только видимых строк), отсутствие артефактов, задержек, ошибок при прокрутке, обновлении диапазона.
Корректная интеграция с кастомным hex-редактором, поддержка всех событий, навигации, выделения, обновления данных.
Возможность расширения (новые инструкции, режимы, форматы, модули анализа) без переписывания ядра, через интерфейсы и точки расширения.
Подробные логи для отладки, анализ ошибок, предупреждений, событий, производительности.
Документация и примеры использования для всех основных сценариев, модулей, интерфейсов, расширений.
10.2. Нефункциональные критерии
Время декодирования 1000 инструкций — не более 100 мс на среднестатистическом ПК (Intel i5, 8 ГБ RAM), при больших объёмах — линейное масштабирование.
Время обновления диапазона строк — не более 50 мс, отсутствие заметных задержек при прокрутке, навигации.
Потребление памяти — не более 50 МБ на 1 МБ кода, оптимизация хранения промежуточных данных.
Корректная обработка ошибок и исключений, отсутствие сбоев, утечек памяти, зависаний.
Соответствие архитектурным принципам (разделение логики и UI, использование интерфейсов, абстракций, точек расширения).
...
18.4. Ограничения и реалистичные цели
Создание такого анализатора — задача, сравнимая по сложности с разработкой компилятора или переводчика между двумя сложнейшими языками. x86-ассемблер допускает произвольные переходы, самомодифицирующийся код, нестандартные прологи/эпилоги, что делает автоматическое определение границ и назначения функций крайне сложным. Даже лучшие инструменты (IDA, Ghidra) не всегда справляются идеально и всегда оставляют место для ручной корректировки.
В связи с этим:
Анализатор позиционируется как "помощник", а не "заменитель эксперта".
Основная задача — автоматизация рутинных операций (поиск функций, построение call graph, подсказки по паттернам), а не "магическое" понимание смысла любого кода.
Всегда должна быть возможность ручной корректировки, переименования, комментирования, визуализации.
Архитектура должна быть расширяемой: поддержка новых эвристик, паттернов, сигнатур, интеграция с внешними базами, возможность дообучения.
Вся работа анализатора должна быть прозрачной для пользователя: показывать, почему принято то или иное решение, давать возможность "откатить" или скорректировать результат.
18.5. Польза для реверса и обучения
Существенно ускоряет первичный разбор неизвестного бинарного кода.
Помогает быстро выявить ключевые функции, точки входа, связи между частями программы.
Позволяет отслеживать реальные пути исполнения, видеть "живое" поведение функций.
Делает процесс обучения ассемблеру более наглядным: пользователь видит, как реально исполняется код, какие участки "живые", а какие — только "на бумаге".
Обеспечивает удобную платформу для накопления и обмена знаниями (база сигнатур, паттернов, пользовательских комментариев).
mov eax, [ebx]
add eax, 4
call eax
Это даже не боль - мелочь кошачья и легко понимается
Берём значение из памяти по адресу в ebx → кладём в eax
Прибавляем 4 к этому значению.
Вызываем функцию по получившемуся адресу.
Такой шаблон очень характерен для C++ при вызове виртуальных функций через полиморфный объект
class Animal {
public:
virtual void speak() { cout << "Animal sound\n"; }
virtual void move() { cout << "Animal moves\n"; } // ← это вторая виртуальная функция
};
Animal* animal = new Dog();
animal->move(); // ← вот этот вызов может компилироваться в эти 3 строки
Поэтому автор и привел три варианта, любой из которых может быть скомпилирован в приведенный asm-код. Вы выбрали всего лишь один из них.
Так я исходил из того, что знаю. Есть два пути, короткий и длинный:
1. Короткий:
1.1. То, что я написал и без вариантов
2. Длинный
2.1. Открываем DIE (Github)
2.2. Открываем ImHex (Github)
Согласно пунктам 2.1. и 2.2. получили базовую инфу о программе
2.3. Если у нас C то:
С (пример)
struct GameModule {
void (*init)();
void (*update)();
void (*render)();
};
GameModule* mod = ...;
mod->update(); // → mov eax, [ebx] → add eax, 4 → call eax
2.4. Если у нас C++ - пример уже написан
2.5. Есть вариант что это указатели на функции, которые хранятся в структуре, загруженной из DLL, но обычно, они так редко записываются
3. Открываем Гидру, Иду или другое...
Итог:
Если видишь, что ebx указывает на объект, а [ebx] — на глобальную таблицу, и так везде — тогда можно сказать: "Да, это vtable." В противном случае лучше сказать: "индиректный вызов через таблицу функций".
Как мыслит дизассемблер: внутренняя логика decompiler-инструментов на примере Ghidra и RetDec