Дизассемблирование применяется, когда нужно понять, что делает скомпилированная программа, исходный код которой недоступен: например, при анализе подозрительного исполняемого файла, поиске уязвимости в прошивке роутера или изучении чужой библиотеки без документации. В отличие от чтения исходников, здесь аналитик работает с готовым бинарным файлом — последовательностью байтов, которую процессор исполняет напрямую.
Проще говоря, дизассемблирование — это процесс преобразования машинного кода (бинарных инструкций) обратно в текст на языке ассемблера, понятный человеку. Результат не восстанавливает исходную программу на C, Python или другом языке высокого уровня, но позволяет увидеть логику работы: какие функции вызываются, как обрабатываются данные, где происходят проверки условий и переходы. Именно на этом этапе начинается большинство задач обратной инженерии.
Как программа превращается в машинный код
Чтобы понять суть дизассемблирования, нужно вспомнить обратный путь. Разработчик пишет код на языке высокого уровня, затем компилятор переводит его в машинные инструкции — последовательности байтов, специфичные для конкретной архитектуры процессора (x86, x86-64, ARM и других). На этом этапе теряются имена переменных, комментарии, структура классов и большая часть логических конструкций.
Ассемблер занимает промежуточное положение: это человекочитаемое представление тех же машинных инструкций, где каждой операции соответствует мнемоника вроде MOV, JMP или CALL. Дизассемблер выполняет ровно этот шаг — из байтов восстанавливает мнемоники. Обратный процесс, перевод ассемблера в машинный код, называется ассемблированием.
Важный нюанс: преобразование «ассемблер → машинный код» почти однозначно, а вот обратное — нет. В бинарном файле код и данные перемешаны, поэтому дизассемблеру приходится угадывать, где заканчивается инструкция и начинается, например, таблица строк. Отсюда основные сложности анализа, о которых речь пойдёт ниже.
Как работает дизассемблер
Существует два базовых алгоритма дизассемблирования, и современные инструменты обычно комбинируют оба.
- 🔹 Линейное сканирование (linear sweep) — дизассемблер читает байты последовательно от начала секции кода, декодируя инструкции одну за другой. Метод быстрый, но ошибается, если внутри кода встречаются встроенные данные: декодер начнёт трактовать их как инструкции и «поедет» дальше по потоку.
- 🔹 Рекурсивный спуск (recursive traversal) — анализатор следует по потоку управления: после инструкции перехода JMP или вызова CALL он продолжает разбор уже с адреса назначения. Так код отделяется от данных точнее, но часть кода, недостижимого статически (например, вызываемого по вычисляемому указателю), может остаться нераспознанной.
- 🔹 Гибридный подход — сочетает оба метода и дополняет их эвристиками: поиском прологов функций, сигнатур библиотек, анализом таблиц импорта и экспорта.
На выходе вы получаете листинг: адреса инструкций, мнемоники, операнды, а в хороших инструментах — ещё и граф вызовов, перекрёстные ссылки между функциями и восстановленные строки. По этому листингу аналитик восстанавливает алгоритм работы программы.
📊 Какой инструмент для дизассемблирования вы используете чаще всего