
今日は、アプリケーションの逆コンパイルについてお話したいと思います(すべてが同じJavaに適用され、いくつかの仮定と制限がある言語に適用されますが、私自身は.Net開発者なので、例はかなりMSIL'oviziです:))。
はじめに、.Netの世界での現在の逆コンパイルの方法をリストします。
- JetBrains dotPeek (R#ホットキーサポート、キャラクターサーバー)
- Telerik JustDecompile (これも悪くない、多くのホットキー)
- RedGate Reflector ( dotPeekのアナログ、ただし有料。最初は世界の主要な.Netでしたが、今のところ無料です)
- icsharpcode ILSpy (良い、オープンソース。Mono.Cecilを使用してコードを記述するときに役立ちます。これにより、その動作をよりよく理解できます)
- 9rays Spices .Netデコンパイラー
- インプレースエディター機能を備えたDis#
ソフトウェアの逆コンパイルの場合:
- Mono.Cecil (世界で最もクールなデコンパイラ.Net。出力では、アセンブリの内容のオブジェクト「ミラー」を取得します。つまり、IL配列をDOMに変換するなどの機能はありません。
- ICSharpCode.Decompiler (ループ、スイッチ、ifがあるDOMに配列[MSIL]を変換するmono.cecilのアドオン。SharpDevelop/ ILSpyの一部です)
- Harmony Core (私と似ていますが、シンボルに関する情報を保存しています。平均的な状態で、販売準備ができていないので、ヘルプを歓迎します)。
そして今、私はそれらがどのように動作するかを説明したいと思います(JetBrainsマシンがどのように動作するのだろうか?) 少なくともそれがいかに難しいかを理解するために:デコンパイラ.NetアセンブリをC#コードに書き戻します。

まず、このサイクルのHabréに投稿された記事の全リスト
まず、逆コンパイラに一連の要件を設定します。
- すべてのアセンブリを受け入れる必要があります:CLR 1. *〜4. *
- C#出力だけでなく、MSIL、VB.NET、および一般的にもサポートする必要があります-これは想像力とニーズに十分です。
- 言語の異なるバージョン(C#など)から選択する機能。実装に重複はありません。
そして、要件が定義されたので、MSILがどのように機能し、MSILがアプリケーションをすばやく逆コンパイルするのにどのように役立つかを考えてみましょう。
逆コンパイル処理でいくつかの困難(レジスタ、最適化、いくつかの方法で1つのアクションを実行する機能)をもたらすプロセッサ言語とは異なり、MSILではすべてが可能な限り単純です。 ローカル変数に何かを書き込む必要がある場合、このためのコマンドは1つだけです。 別の方法では、変数への書き込みは機能しません。 このプロパティにより、最終的なコンパイラ(JITter)の実装が簡単になります...一方、逆コンパイル側の実装が簡単になります。
MSILの2番目の特性はスタックコンピューティングです。 レジスタはありません。 そして、すべての計算が通過する唯一のメモリはスタックです。 これは、最終プロセッサがスタック全体をすべて計算することを意味するものではありません。 いや これは、簡単にするために、このモデルではすべての計算の説明とMSILの呼び出しを使用することを意味します。 これは私たちにとって何を意味するのでしょうか? これは、パラメーターに関係なく1つのコマンドで、2つの数値を追加できることを意味します。 これは、スタックから追加するデータを取得し、それらを追加して、結果をどこかではなくスタックに保存するコマンドです。 これは重要です。なぜなら、私たちにとって、逆コンパイラを書く人たちにとっては、これはコードの大きな分岐を引き起こさないからです。
次に、最も重要なこと、つまり逆コンパイルプロセスがどのように行われるかについて説明します。
それで
Ldc_i4 5 Ldc_i4 4 Add Stloc.1
取得する
Sum = 5 + 4;
頭に浮かぶ最初の困難:指示の位置が異なる場合があります。 つまり、たとえば、実行されるコードの場合、ldind_i4とaddの間に他の命令がないことはまったく必要ありません。 たとえば、次のコードは完全に有効です。
Ldc_i4 5 Ldc_i4 4 Ldc_i4 10 Stloc.2 // sum2 Add Stloc.1 // sum1
たとえば、次のように逆コンパイルする必要があります。
Sum2 = 10 Sum1 = 5 + 4;
第二に、リリース内の変数の名前が欠落している可能性があります。 つまり 不純物がない場合、コードは次のようになります。
= 10 = 5 + 4;
第三に、最も難しいのはif-else、while、do-while、スイッチの実装が異なる場合があります。 これは、ラムダ、yield、async / awaits、およびその他の言語ガジェットで特に当てはまります。これらはオプションであり、実際に言語の通常の機能の上に実装されます。 これをすべて考慮する方法は? 実際、両方の問題は2つの方法でのみ解決されます。
スタック逆コンパイルモデル
逆コンパイルのために、特定のMSILコードインタープリターが作成されます。これには、独自のスタックとコマンド解釈サイクルがあります。 ループの各反復で、次の未検査のコマンドが取られます。
- これがジャンプ命令ではない場合、調査中のチームがスタック上の値をいくつ必要としているかを調べます。 次に、スタックから2つのコンピューティングノードを取得し、前のコマンドの計算結果としてそこに配置し、スタックから取得したノードを分岐とする新しいノードを作成します。 上記の例では、次のようになります。
つまり まず、4つの入力コマンドがあります。 最初の2つは入り口で何も受け取りませんが、数だけを返します。 したがって、それらをスタックに配置します(ldind_i4 4、ldind_i4 5)。 その後、次のコマンドを実行します-追加。 スタックから2つの入力値を受け取ります。 したがって、チームから結果が得られるため、スタックから2つのノードを読み取り、このコマンドのパラメーターとして保存し、コマンド自体をスタックに保存します。 そして、結果はすべてスタックに保存されます。
さらに、結果はメソッドに渡されるか、他の算術演算に参加するか、retステートメントを使用して返されます。
したがって、式がより複雑な場合:
Ldc_i4 5 Ldc_i4 4 Ldc_i4 10 Mul Add Stloc.1 // value = 5 + (4 * 10)
DOMの作成プロセスは次のようになります。
その後、ツリーの最終アセンブリが実行されます。
メソッド呼び出しも同じ方法で構築されます。 メソッドの場合のみ、呼び出しに必要なパラメーターの数がスタックから取得され、メソッド呼び出しのノードクラスに格納されます。 メソッドが値を返す場合、メソッド呼び出しノードはスタックされます。 そうでない場合は、既製の式のグループに追加されます。
- 遷移命令(条件付きまたは無条件)がある場合、遷移条件を計算するためのノードと、この条件が満たされ、観察されない場合に実行されるノードを含むグループ化ノードが作成されます。 この段階で、これらのパターンが「鳥瞰図」でどのように見えるかを検討します。
木材組立
これらはすべて準備段階でした。 さらに、モジュール性のために、ツリー内の1つの構造を認識し、それを別の構造に変換するクラスが作成されます。 たとえば、if-elseの場合、遷移が前方に実行されるような条件付き遷移の存在が一致します。 次に、ノードはif-elseノードに変換され、遷移後のコードはelse(負のif)ノードとしてマークされ、条件とelseノードの間のコードはノードの場合正としてマークされます。 前の命令への遷移と条件付き遷移として一致する場合、whileループとして一致し、ツリーも再構築されます。 したがって、マッチャーのパフォーマンスの清浄度に応じて、出力では特定のプログラミング言語用に変換されたツリーを取得します。 次に、プログラミング言語ごとに、彼に合った多くのマッチャーを求めます。 たとえば、ループと条件はほとんどすべてのパッケージに存在するため、すべての人に適しています。 そして、ここで、例えば、async / await-C#専用です。 したがって、彼のパッケージのみが存在します。
図をわかりやすくするため、if-elseとwhile / do-whileがどのように組み立てられるかについて、例を検討してください。
IF-ELSEブロックアセンブリ

WHILEブロックを構築

コード生成
一致の最後の段階は、ツリーでのコード生成です。 問題はないはずです。 理想的には、もちろん、R#またはStyleCopからルールを取り入れることはクールです。 幸いなことに、それらはXMLです。 しかし、最も単純なケースでは、クラス記述ツリーを入力として使用するジェネレーターを作成します。 最初にツリー全体をチェックする必要があります。サポートされていないタイプのノードが含まれているかどうか。 すべてが正常な場合、ツリー全体がバイパスされ、StringBuilderおよび対応するノードが渡されるVisitorデザインパターンを使用して、各ノードに対して対応するメソッドが呼び出されます。 さらに、各行の先頭からインデントする必要があるスペースの数を考慮する必要があります。 この段階では、すべてが非常に簡単です。
変数名の生成

コードを生成する前に、コンパイルプロセス中に失われたすべての変数の名前を生成する必要があります。 マッチングアルゴリズムもこのために書かれています。 変数名を生成するには:
- 使用される計算または計算結果で、コンパイラーによって生成されないメソッドの名前。 例:var ??? = this.Counterparty; -> ??? =取引相手。
- 変数がループ変数かどうかのデータ。 つまり サイクルの本体でのみ考慮されますか。 整数の場合、名前の候補はインデックスi、jです。
- foreachループの変数が反復可能なコレクションの要素である場合、[collectionName] Itemまたは単にitemと呼ぶことができます。
これらおよび他の多くのアルゴリズムを実装するために、特定のプログラミング言語用にツリーを再配置する仲人に似たマッチャーが使用されます。
次は?
次に、特定の逆コンパイラを分析します。 dotPeek dotPeek、セシル、そして私のもの。 しかし、これは少し後です=)