- PVSM.RU - https://www.pvsm.ru -
Сегодня хотелось бы поговорить про декомпиляцию приложений (все применительно к той же Java, да и любому языку с некоторыми допущениями и ограничениями, но поскольку сам я — .Net разработчик, примеры будут совсем немного MSIL'овизированы :) ).
Для вводной, перечислю текущие средства декомпиляции в мире .Net:
Для программной декомпиляции:
А теперь, хотелось бы описать как они работают (вам же интересно, как работает машинка от JetBrains?). Чтобы как минимум понять, насколько это сложно: написать свой декомпилятор .Net сборки обратно в код на C#.
Для начала, заложим в наш декомпилятор набор требований:
И теперь, когда требования определены, давайте подумаем, как устроена работа MSIL, и как это поможет нам в быстрой декомпиляции приложения.
В отличии от языка процессора, который вносит для нас некоторые сложности в процесс декомпиляции (регистры, оптимизации, возможность сделать одно действие несколькими способами), в MSIL все максимально просто. Если надо записать в локальную переменную нечто, то для этого есть всего одна команда. Другим способом записать в переменную значение не получится. Это свойство наделяет конечный компилятор (JITter) простотой в реализации с одной стороны… А с другой стороны наделяет простотой в реализации декомпилирующую сторону.
Второе свойство, каким обладает MSIL, это вычисления на стеке. Тут нет регистров. И единственная память, через которую идут все вычисления — это стек. Это абсолютно не значит что конечный процессор также все вычисляет через стек. Нет. Это значит что этой моделью для упрощения пользуется описание всех расчетов и вызовов на MSIL. Что это значит для нас? Это значит что сложить два числа можно только одной командой, которая вне зависимости от параметров — одна. Это команда, вытащив данные для сложения из стека, складывает их и сохраняет результат не куда-либо, а обратно в стек. Это важно, потому что для нас, как для людей, пишущих декомпилятор это не породит огромного ветвления кода.
Теперь мы подошли к самому главному: как происходит процесс декомпиляции.
Чтобы после
Ldind_i4 5
Ldind_i4 4
Add
Stloc.1
Получить
Sum = 5 + 4;
Первая трудность, которая приходит в голову: положение инструкций может быть различным. Т.е., например, чтобы код выполнился, совсем не обязательно что между ldind_i4 и add не будет других инструкций. Например, совершенно валиден следующий код:
Ldind_i4 5
Ldind_i4 4
Ldind_i4 10
Stloc.2 // sum2
Add
Stloc.1 // sum1
Что должно декомпилироваться, например, так:
Sum2 = 10
Sum1 = 5 + 4;
Во-вторых названия переменных в релизе могут отсутствовать. Т.е. без примесей, код будет таким:
= 10
= 5 + 4;
В третьих, что самое сложное, реализации if-else, while, do-while, switch могут отличаться. Этого касаются, в особенности, лямбды, yields, async/awaits и прочие языковые примочки, которые являются опциональными и на самом деле реализуются поверх обычных функций языка. Как все это учесть? На самом деле оба вопроса решаются всего двумя способами.
Для декомпиляции пишется некий интерпретатор кода на MSIL, у которого есть свой стек и цикл интерпретации команд. На каждой итерации цикла, берется очередная не рассмотренная команда:
Далее результат может быть передан в метод, либо участвовать в других арифметических операциях, либо возвращен с помощью инструкции ret.
Соответственно, если бы выражение было бы посложнее:
Ldind_i4 5
LdInd_i4 4
LdInd_i4 10
Mul
Add
Stloc.1
// value = 5 + (4 * 10)
То процесс создания DOM выглядел бы следующим образом:
После чего осуществляется окончательная сборка дерева:
Таким же образом конструируются вызовы методов:
Это все были подготовительные этапы. Далее, для модульности, создаются классы, которые распознают какую-либо одну конструкцию в дереве и переводят ее в другую. Например, если это if-else, то матчится наличие условного перехода такого, чтобы переход осуществлялся вперед. Тогда узел преобразуется в if-else ноду, код за переходом помечается как else (negative if) нода, а код между условием и else нодой — как positive if нода. Если матчится как условный переход с переходом на прошлые инструкции, то это матчится как while цикл и дерево также перестраивается. Соответственно, в зависимости от чистоты исполнения матчеров, на выходе мы получем преобразованное дерево под конкретный язык программирования. Далее, у каждого из языков программирования мы задаем множество матчеров, которые ему подходят. Например, циклы и условия подойдут всем, потому они будут присутствовать почти во всех пакетах. А вот, например, async/await — он только для C#. Потому, будет присутствовать тольк в его пакете.
Для ясности картины, как собираются if-else и while/do-while, рассмотрим примеры:
Последний этап матчинга — генерация кода по дереву. Тут не должно быть каких-то сложностей. Идеально, конечно, было бы круто подсасывать правила от R# или StyleCop. Благо, они в XML. Но в простейшем случае, мы пишем генератор, который принимает на вход дерево описания класса. Он сперва обязан проверить все дерево: содержит ли оно не поддерживаемые типы нод. Если все в порядке, то обходится все дерево и для каждого узла вызывается соответствующий метод по шаблону проектирования Visitor, которому передается StringBuilder и соответствующая нода. Дополнительно, необходимо считать количество пробелов, которые надо отступать с начала каждой строки. На этом этапе все достаточно просто.
Перед генерацией кода необходимо сгенерировать имена всех переменных, которые были потерты в процессе компиляции. Для этого также написаны алгоритмы матчинга. Для генерации имен переменных служат:
Для реализации этих и многих других алгоритмов служат матчеры, которые аналогичны матчерам, перестраивающим дерево под конкретный язык программирования.
Автор: sidristij
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/75402
Ссылки в тексте:
[1] JetBrains dotPeek: https://www.jetbrains.com/decompiler/
[2] RedGate Reflector: http://www.red-gate.com/products/dotnet-development/reflector/
[3] icsharpcode ILSpy: http://ilspy.net/
[4] Mono.Cecil: https://github.com/jbevain/cecil
[5] ICSharpCode.Decompiler: https://github.com/icsharpcode/ILSpy/tree/master/ICSharpCode.Decompiler
[6] Harmony : https://github.com/mumusan/harmony
[7] Источник: http://habrahabr.ru/post/244095/
Нажмите здесь для печати.