Как работает дизассемблер и как он отличается от декомпилятора?
Я изучаю установку дизассемблера (или декомпилятора) на моей ОС Linux Mint 17.3, и я хотел знать, какая разница между дизассемблером и декомпилятором. Я имею общее представление о том, что они (имена довольно понятны), но они все еще немного запутывают.
Я читал, что дизассемблер превращает программу в язык ассемблера, чего я не знаю, поэтому для меня это кажется бесполезным. Я также прочитал, что декомпилятор превращает «двоичный файл» в его исходный код. Что такое двоичный файл?
По-видимому, декомпиляторы не могут декомпилировать C, только Python и другие подобные языки. Итак, как я могу превратить программу в исходный исходный код?
Дисассемблер — довольно простое приложение, которое передает машинный код в инструкции языка ассемблера. Это действие — это обратная операция, выполняемая программой ассемблера и простая, потому что между машинным кодом и сборкой существует строгая взаимно однозначная взаимосвязь. Дисассемблер нацелен на определенный процессор. Исходный ассемблер, который использовался для создания исполняемого файла, имеет лишь незначительную значимость.
Декомпилятор нацелен на воссоздание скомпилированной языковой программы высокого уровня из машинного кода в исходный формат. Таким образом, попытка обратной работы C или Forth (популярные языки, для которых существуют компиляторы). Поскольку существует так много языков высокого уровня и, следовательно, так много способов, как оригинальные языковые конструкции высокого уровня могут быть выражены в машинных кодах (даже много разных стратегий для одного и того же языка и конструкции, даже в одном и том же компиляторе, и даже разные стратегии в зависимости от режима компилятора и ситуации), эта операция намного сложнее и очень зависит от исходного компилятора (и, возможно, даже от используемой командной строки, выбрала уровень оптимизации, а также используемую версию).
Даже если все это подходит, большая часть работы декомпилятора — это угаданное угадывание и, скорее всего, никогда не достигнет точки, где он сможет восстановить исходную программу в форме исходного кода 100%. Это скорее закончится версией исходного кода это могла быть оригинальная программа.
Что внутри у .wasm-файла? Знакомство с wasm-decompile
В нашем распоряжении имеется множество компиляторов и других инструментов, позволяющих создавать .wasm-файлы и работать с ними. Количество этих инструментов постоянно растёт. Иногда нужно заглянуть в .wasm-файл и разобраться с тем, что у него внутри. Может быть, вы — разработчик одного из Wasm-инструментов, или, возможно, вы — программист, который пишет код, рассчитанный на преобразование в Wasm, и интересующийся тем, как выглядит то, во что превратится его код. Такой интерес может быть вызван, например, соображениями производительности.

Проблема заключается в том, что в .wasm-файлах содержится довольно-таки низкоуровневый код, который сильно похож на настоящий ассемблерный код. В частности, в отличие, например, от JVM, все структуры данных компилируются в наборы операций load/store, а не в нечто такое, в чём имеются понятные имена классов и полей. Компиляторы, вроде LLVM, могут так изменить входной код, что то, что у них получается, и близко на него не похоже.
Как быть тому, кто хочет, взяв .wasm-файл, узнать о том, что в нём происходит?
Дизассемблирование или… декомпиляция?
Для преобразования .wasm-файлов в файлы .wat, содержащие стандартное текстовое представление Wasm-кода, можно воспользоваться инструментами наподобие wasm2wat (это — часть набора инструментов WABT). Результаты такого преобразования очень точны, но читать получившийся код не особенно удобно.
Вот, например, простая функция, написанная на C:
Код хранится в файле dot.c .
Воспользуемся следующей командой:
Далее, чтобы преобразовать то, что получилось, в .wat-файл — применим следующую команду:
Вот что это нам даст:
Код это маленький, но его, по многим причинам, крайне тяжело читать. Помимо того, что тут не используются выражения, и того, что он, в целом, выглядит достаточно многословно, нелегко разобраться в структурах данных, представленных в виде команд по загрузке данных из памяти. А теперь представьте, что вам нужно проанализировать подобный код гораздо большего размера. Такой анализ станет весьма сложной задачей.
Попробуем, вместо использования wasm2wat, выполнить следующую команду:
Вот что она нам даст:
Это выглядит уже гораздо лучше. Помимо того, что тут используются выражения, напоминающие уже известный вам язык программирования, декомпилятор разбирает команды, направленные на работу с памятью, и пытается воссоздать структуры данных, представленные этими командами. Затем система аннотирует каждую переменную, которая используется как указатель с «встроенным» объявлением структуры. Декомпилятор не создаёт именованное объявление структуры, так как он не знает о том, есть ли что-то общее между структурами, в которых используются по 3 float-значения.
Как видите, результаты декомпиляции оказались более понятными, чем результаты дизассемблирования.
На каком языке написан код, выдаваемый декомпилятором?
Инструмент wasm-decompile выводит код, пытаясь сделать этот код похожим на некий «усреднённый» язык программирования. При этом данный инструмент старается не уходить слишком далеко от Wasm.
Первая цель wasm-decompiler — формирование читабельного кода. То есть — такого кода, который позволит его читателю легко разобраться в том, что происходит в декомпилированном .wasm-файле. Вторая цель этого инструмента заключается в том, чтобы выдать как можно более точное представление .wasm-файла, сформировав код, который полно представляет то, что происходит в исходном файле. Очевидно то, что эти цели далеко не всегда хорошо друг с другом согласуются.
То, что выводит wasm-decompiler, изначально не задумывалось как код, представляющий некий реальный язык программирования. Сейчас нет способа скомпилировать этот код в Wasm.
Команды загрузки и сохранения данных
Как показано выше, wasm-decompile ищет команды загрузки и сохранения данных, связанные с конкретным указателем. Если эти команды формируют непрерывную последовательность, декомпилятор выводит одно из «встроенных» объявлений структуры данных.
Если обращались не ко всем «полям», декомпилятор не может с уверенностью отличить структуру от некоей последовательности операций по работе с памятью. В таком случае wasm-decompile использует резервный вариант, применяя более простые типы вроде float_ptr (если типы являются одинаковыми), или, в худшем случае, формирует код, иллюстрирующий работу с массивом, наподобие o[2]:int . Такой код говорит нам о том, что o указывает на элементы типа int , и мы обращаемся к третьему такому элементу.
Эта вот последняя ситуация возникает гораздо чаще, чем можно подумать, так как локальные Wasm-функции больше ориентированы на использование регистров, а не переменных. В результате в оптимизированном коде один и тот же указатель может использоваться для работы с совершенно не связанными друг с другом объектами.
Декомпилятор стремится интеллектуально подходить к индексированию и способен выявлять паттерны наподобие (base + (index << 2))[0]:int . Источником таких паттернов являются обычные для C операции индексирования, наподобие base[index] , где base указывает на 4-байтный тип. В коде это встречается очень часто, так как Wasm, в командах загрузки и сохранения данных, поддерживает лишь смещения, задаваемые в виде констант. В коде, формируемом wasm-decompile, подобные конструкции преобразуются к виду base[index]:int .
Кроме того, декомпилятор знает о том, когда абсолютные адреса указывают на раздел данных.
Управление потоком выполнения программы
Если говорить об управляющих конструкциях, то самой известной среди них является Wasm-конструкция if-then, которая превращается в if (cond) < A >else < B >, с дополнением того, что в Wasm такая конструкция может возвращать значение, поэтому она может представлять и тернарный оператор, вроде cond ? A : B , который есть в некоторых языках.
Другие управляющие конструкции Wasm основаны на блоках block и loop , а также на переходах br , br_if и br_table . Декомпилятор старается держаться как можно ближе к этим конструкциям. Он не стремится к тому, чтобы воссоздать конструкции while/for/switch, которые могли бы послужить основой для них. Дело в том, что такой подход лучше показывает себя при обработке оптимизированного кода. Например, обычная конструкция loop может выглядеть в коде, выдаваемом wasm-decompile, так:
Здесь A — это метка, которая позволяет строить вложенные друг в друга конструкции loop . То, что тут есть команды if и continue , используемые для управления циклом, может выглядеть несколько чужеродно для циклов while, но они соответствуют Wasm-конструкции br_if .
Блоки оформляются похожим образом, но тут условия находятся в начале, а не в конце:
Здесь показан результат декомпиляции конструкции if-then. В будущих версиях декомпилятора, вероятно, вместо такого кода, там, где это возможно, будет формироваться более привычная конструкция if-then.
Самое необычное средство Wasm, использующееся для управления потоком выполнения программы, это br_table . Это средство представляет собой нечто вроде оператора switch, за исключением того, что тут используются встроенные блоки. Всё это усложняет чтение кода. Декомпилятор упрощает структуру подобных конструкций, стремясь к тому, чтобы немного облегчить их восприятие:
Это напоминает использование switch для анализа a , когда вариантом, используемым по умолчанию, является D .
Другие интересные возможности
Вот ещё некоторые возможности wasm-decompile:
- Декомпилятор может извлекать имена из отладочных данных или из данных компоновки, а также может генерировать имена самостоятельно. При использовании существующих имён предусмотрено упрощение искажённых C++-имён.
- Система уже поддерживает предложение, касающееся, кроме прочего, возврата из функции нескольких значений. Это немного усложняет превращение исходного кода в выражения и инструкции. Если функции возвращают несколько значений, используются дополнительные переменные.
- Имена могут быть сгенерированы на основании содержимого раздела данных.
- Декомпилятор формирует аккуратные объявления для всех типов разделов Wasm-файлов, а не только для кода. Например, wasm-decompile пытается улучшить читабельность разделов данных, выводя их, если это возможно, в виде текста.
- Система пытается уменьшить количество скобок в выражениях, учитывая приоритет операторов (по правилам, которыми обычно пользуются в C-подобных языках).
Ограничения
Декомпиляция Wasm-кода — это задача, которая гораздо сложнее, чем, например, декомпиляция байт-кода JVM.
Байт-код не подвергается оптимизации, то есть — довольно точно воспроизводит структуру исходного кода. При этом, несмотря на то, что в таком коде могут отсутствовать исходные имена, в байт-коде используются ссылки на уникальные классы, а не на области памяти.
В отличие от байт-кода JVM, код, попадающий в .wasm-файлы, сильно оптимизирован LLVM. В результате такой код часто теряет большую часть исходной структуры. Выходной код очень не похож на то, что написал бы программист. Это значительно усложняет задачу декомпиляции Wasm-кода с выводом результатов, способных принести программистам реальную пользу. Однако это не означает, что мы не должны стремиться к решению этой задачи!
Итоги
Если вам интересна тема декомпиляции Wasm-кода, то, пожалуй, лучший способ в этой теме разобраться — взять и декомпилировать собственный .wasm-проект! Кроме того, здесь вы можете найти более подробное руководство по wasm-decompile. Код декомпилятора можно найти в файлах этого репозитория, имена которых начинаются с decompile (если хотите — присоединяйтесь к работе над декомпилятором). Здесь можно найти тесты, показывающие дополнительные примеры различий между .wat-файлами и результатами декомпиляции.
Чем отличается декомпиляция от дизассемблирования

Дизассемблеры и декомпиляторы исполняемых файлов
В комментариях к статьям меня часто спрашивают где взять тот или иной инструмент, используемый в исследовании. По возможности я всегда указываю ссылки, но теперь настало время самых мощных инструментов, а именно дизассемблеров и декомпиляторов исполняемых файлов. Сразу уточню терминологию. Дизассемблирование — преобразование программы из двоичного кода к ее ассемблерному представлению. Декомпиляция — процесс воссоздания исходного кода программы.

Скриншот программы dnSpy
Начнем с популярного нынче дотнета. Не будет преувеличением сказать, что самый мощный на сегодняшний день инструмент для потрошения приложений на .NET — это бесплатный проект dnSpy. Он включает в себя декомпилятор C# и Visual Basic .NET, отладчик, редактор сборки с подсветкой синтаксиса, HEX-редактор и еще множество инструментов. Русский язык в наличии. Самую свежую версию можно всегда скачать с офсайта.

Скриншот программы IDA Pro Advanced
IDA Pro (сокращение от Interactive DisAssembler) — один из моих основных инструментов для реверс-инжиниринга и разбора файлов. Это интерактивный дизассемблер и отладчик с поддержкой множества форматов исполняемых файлов для большого числа процессоров и операционных систем. Чтобы перечислить все его возможности потребуется целая книга. Но даже тут возможности IDA не заканчиваются. Плагин Hex-Rays для IDA Pro позволяет декомплировать ассемблерный листинг в более-менее человекопонятный псевдокод, по синтаксису похожий на C. В некоторых случаях это значительно облегчает работу. Просто так приобрести IDA Pro частным лицам практически невозможно, и дело не только в непомерной цене, а в том, что автор придерживается абсолютно неадекватной политики в плане продаж. К счастью, несколько последних версий этого замечательного дизассемблера, несмотря на все трудности, были успешно слиты в свободный доступ. Это IDA Pro Advanced 6.8, последняя доступная версия, которая работает с 32-битными системами, а также IDA Pro Advanced 7.0 и IDA Pro Advanced 7.2 для 64-битных систем. Если по каким-то причинам вы не можете использовать варез, то на офсайте есть бесплатные демо-версии с урезанным функционалом.

Скриншот программы Interactive Delphi Reconstructor
IDR (Interactive Delphi Reconstructor) — бесплатный декомпилятор исполняемых файлов и динамических библиотек. В отличие от IDA Pro, этот декомпилятор создан специально для разбора файлов, написанных на языке Delphi. Сейчас проект прекратил развитие, если какие изменения и вносятся, то исключительно косметические. Исходники для доработки открыты. Лично я пользуюсь стабильным комплектом Interactive Delphi Reconstructor 2.6.0.1.

Скриншот программы VB Decompiler Pro
Еще один специализированный декомпилятор — VB Decompiler Pro. Он работает с программами (EXE, DLL, OCX), написанными на Visual Basic. В случае, если приложение собрано в p-code, декомпилятор может разобрать его практически до исходного кода. Но даже если приложение скомпилировано в native code, в этом случае VB Decompiler анализирует и восстанавливает довольно много инструкций, чтобы насколько это возможно приблизить ассемблерный код к исходному. Это сильно упростит задачу анализа алгоритмов исследуемой программы. Честные граждане могут воспользоваться бесплатной Lite-версией с офсайта, для любителей полных версий софта есть релиз VB Decompiler Pro 10.0. Антивирусы могут ругаться на активатор, но тут вы уже сами решайте что делать.
Конечно, это далеко не полный список инструментов для дизассемблирования и декомпиляции, который есть в свободном доступе. Например, та же набирающая популярность Ghidra от АНБ может составить конкуренцию IDA Pro с Hex-Rays. Но я в этой статье перечислил лишь те программы, которыми пользуюсь сам и которые упоминаются в статьях на этом сайте.
Как работает декомпиляция?
Я уже несколько раз слышал термин «декомпиляция», и мне становится очень любопытно, как это работает.
У меня есть очень общее представление о том, как это работает; реконструировать приложение, чтобы увидеть, какие функции оно использует, но я мало что знаю об этом.
Я также слышал термин «дизассемблер«, в чем разница между дизассемблером и декомпилятором?
Итак, резюмируя мой вопрос (вопросы): Что именно участвует в процессе декомпиляции чего-либо? Как это обычно делается? Насколько это сложный/легкий процесс? может ли он произвести точный код? И в чем разница между декомпилятором и дизассемблером?

2 ответы
Ильфак Гильфанов, автор Декомпилятор Hex-Rays, выступил с речью о внутренней работе своего декомпилятора на какой-то афере, и вот технический документ и presentation. Это описывает хороший обзор всех трудностей при создании декомпилятора и того, как заставить все это работать.
Что касается сложности, то вся «декомпиляция» зависит от языка и времени выполнения бинарника. Например, декомпиляция .NET и Java считается «выполненной», поскольку существуют доступные бесплатные декомпиляторы с очень высоким коэффициентом успеха (они создают исходный код). Но это вызвано очень специфическим характером виртуальных машин, которые используют эти среды выполнения.
Что касается действительно компилируемых языков, таких как C, C++, Obj-C, Delphi, Pascal. задача становится намного сложнее. Подробности читайте в вышеуказанных документах.
в чем разница между дизассемблером и декомпилятором?
Когда у вас есть двоичная программа (исполняемый файл, библиотека DLL и т. д.), она состоит из инструкций процессора. Язык этих инструкций называется сборка (или ассемблер). В двоичном коде эти инструкции двоично закодированы, так что процессор может выполнять их напрямую. А дизассемблер берет этот двоичный код и переводит его в текстовое представление. Этот перевод обычно 1-к-1, то есть одна инструкция отображается как одна строка текста. Эта задача сложная, но понятная, программе просто нужно знать все различные инструкции и то, как они представлены в двоичном виде.
С другой стороны, декомпилятор выполняет гораздо более сложную задачу. Он берет либо двоичный код, либо вывод дизассемблера (что в основном одно и то же, потому что это 1-к-1) и создает высокоуровневый код. Позвольте мне показать вам пример. Скажем, у нас есть эта функция C:
Когда вы компилируете его, компилятор сначала генерирует файл сборки для этой функции это может выглядеть примерно так:
(первая строка просто метка, а не настоящая инструкция, SHL выполняет операцию сдвига влево, которая быстро умножает на два, RET означает, что функция выполнена). В результирующем бинарнике это выглядит так:
(Я придумал это, а не настоящие двоичные инструкции). Теперь вы знаете, что дизассемблер переводит вас из бинарной формы в ассемблерную форму. А декомпилятор переводит вас из ассемблерной формы в код C (или какой-либо другой язык более высокого уровня).