kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

タグアーカイブ: MSIL

.NET 基礎勉強会で IL の話をしてきました


資料現物とかは以下

PowerPoint のファイル http://sdrv.ms/12Gh11k

ソースとかはこちら https://github.com/kazuk/SimpleILer/

 

比較的簡単(当社比)なコードで、 MSIL を逆アセンブル、制御フロー解析、データフロー解析をやっています。

コアロジックは Msil クラス に実装されていて、上から順に ILInstructions で命令の切り出し、GetRuns で基本ブロックに分解、PopulateControlFlowPath で制御フローパスの列挙、 SimulateEvalStack で評価スタックのシミュレーションをやり、結果として各 IL 命令のデータ依存グラフが得られます。

基本ブロック

簡単に言えば、制御構造単位での命令列です。基本ブロックでは末尾を除いて制御フロー命令が無いようになっています。また、基本ブロックの途中には飛び込まない(制御フローが飛び込む場所は基本ブロックの先頭になる)ように切り出しされます。

[制御フロー命令ではない一連の命令] optional [制御フロー命令]

制御フロー命令が無い基本ブロックの末尾には、次のブロックへの無条件分岐が暗黙に存在すると考えても一緒です。

基本ブロックを導入する事で、制御フローの列挙が非常に簡単になります。各基本ブロック内には制御フロー命令は存在しませんから基本ブロック内の命令を完全取り去って nop にしてしまっても、トポロジー的な意味でのメソッド内での制御構造は変化しません。逆にMSIL を吐き出したく、その中である程度複雑な制御トポロジーを扱いたい(=コンパイラを書きたい)なら複数の基本ブロックを作り上げてその基本ブロックを出力ILバイト列にどういう順序で出力するか、基本ブロックを接続する制御フロー命令は何を使えば良いかという順序で考える事になります。

動作フローのパス列挙

基本ブロックに分解された結果の基本ブロックの末尾命令が示すブロック間の接続を Queue を使った幅優先探索をやっています。

例外とかで飛び出す、飛び込む物を認識したい場合にはもうちょっと細工が要りますが要点は一緒です。

単純に無条件分岐なら分岐先基本ブロックが次の探索対象、条件分岐であれば条件成立時と非成立時の両方を探索するようにキューすることになります。当然に多分岐なら多分岐のすべての分岐先を探索します。

評価スタックのシミュレーションとデータフロー解析

今回の実装では評価スタックの要素型は意識していません。何個のデータを Push するかによって命令オフセットをスタックに積み、何個のデータを Pop するかにしたがって、スタックに積まれた命令オフセットを取り込んで、どの命令が積んだデータをどの命令が受け取るかという事だけを求めています。命令オフセットだけあれば再度 IL バイト列を見てあげれば実際の操作もわかるし、そのスタック要素の型も求まりますのでそこのコストが低ければ解析結果に入れる必要とか無いんじゃないのって事です。

デコンパイルやクロスプラットフォーム変換

この関係性を元に “データを受け取る IL 命令 ( データを出したIL命令 , … )” の形式で出力された構造がデモの最後に表示された変な式構造です。 IL 命令が add だったら + を二項演算子の中置記法でだすとかの判定と変換をしつこくやっていけば普通の言語での式になるでしょう。表示せずに式の木構造を何かに出せばもっとなんかできるかもしれません。(.NETの式木のノードを出力すれば式木になるでしょう) LLVM Instruction で出せばLLVMバックエンドでコンパイルできるはずでしょう。

この辺はやりたきゃどうぞの世界です。

まとめ

こんな基礎の上に .NET が乗っかってるんだって話で MSIL 読もうぜ、コードで!って話でした。

自分的には制約ソルバーとか証明って方に進んでいきたいんだけど、それをするには僕の人生ではまだ時間が足りていない感じで妄想段階を出ていませんです。

広告

普通の人は全く知らないでもいい MSIL の基礎知識


 

.NET基礎勉強会 http://connpass.com/event/2441/ で ILについてお話させていただく事になったんですが、まぁ 30分枠ぐらいだと、だいぶ話せる事が限られるんで、あらかじめ Blog に記事乗せといた方が良いかなー的に書いておきます。

 

「手元にございますMSIL命令表をご覧ください」 「えっ、どこよ?」

.NET Framework がインストールされている環境であれば、MSIL命令表は入っています。mscorlibアセンブリ、 System.Refrection.Emit 名前空間配下の OpCodes クラスのOpCode型フィールドをリフレクションで舐めてください。

命令表としての活用の仕方にもよりますが、プレフィックスバイト等も命令表には入っています (Prefix1 etc)、自分の用途でプレフィックスとか要らない場合には、そういう物をフィルタしてあげましょう。 OpCode の OpCodeType を見ればそれがプレフィックスなのか判別する事ができます。

MSIL バイト列の見かた

さて、MSILバイト列の見かたです。バイト列そのものは実行時にリフレクションで取るならば、 MethodInfo から MethodBody を引き、MethodBody から GetILAsByteArray メソッドで取得する事ができます。

このバイト列に各 IL 命令がどのように入っているかです。

オペコード

まず、IL オペコードには 1バイトと 2バイトがあります。

命令のサイズは OpCodes を舐めているならば、OpCode の Size プロパティから取得できます。 これが 1の物は 1バイト命令で、2の物は2バイト命令です。

オペコードの値そのものは OpCode 構造体の Value プロパティに入っています。

2バイトのオペコードはMSILバイト列にビッグエンディアンのバイト順で入ります。要するに 0xDEAD の Value を持つ命令のオペコードが 0xDE 0xAD の順でILバイト列に入ります。(1バイトのプレフィックス命令と、1バイト命令がつながって入っているともいえます。) ※ MSILバイト列で唯一のビッグエンディアンです、他はすべてリトルエンディアンになっています。

1バイトオペコード命令

OpCode オペランド(無い場合もある)

2バイトオペコード命令

Prefix
OpCode
OpCode
(LowByte)
オペランド(無い場合もある)

オペランド

オペランドはオペコードによりますが、 OpCode 構造体の OperandType で取得できます。

OperandType の switch だけが特殊ですが、それ以外は単純にオペランドのサイズは固定されています。そのバイト数分をリトルエンディアンとして読みだせばオペランド値として使える値を普通に取得する事ができます。

OperandType が switch の場合、switch のオペランドにはラベル数が入ります、ラベル数分 InlineBrTarget がバイト列に入ります。

ブランチオフセットの起点

ブランチオフセットの起点はブランチ命令の次の命令です。

たとえば IL Offset n に ShortInlineBrTarget で 3 を指定する1バイトのブランチ命令があった場合、 n +1 (opcode size ) +1 (operand size) がブランチオフセットの起点となり、n +1 (opcode size ) +1 (operand size) + 3(operand value) がブランチ先のIL Offset になります。

ILの動作フローを変える物はブランチ命令と、ret、throwとそれに関連する例外処理ぐらいしかありません。ブランチ命令のオフセットを解釈できるようになった今あなたはILのコントロールフロー解析ができるようになったという事です。

まとめ

7/20 日に話す事はここから後の話ですって事で、MSILの基礎の基礎でした。

7/20 日にはMSILの評価スタックと例外フレームについて話したいと思っているのですが、時間枠的に評価スタックの話で一杯かもしれません。