はじめに
実践的な開発経験のある.NET Frameworkアプリケーションプログラマであれば、ILDASMを使ってプログラムを逆アセンブルしたコードを見たことがあるでしょう。.NET Frameworkアプリケーション開発は、Visual BasicやC#、C++/CLIなど、多くの言語を使って開発できますが、最終的に生成されるのは標準化されているCIL(Common Intermediate Language)と呼ばれる中間言語です。Microsoft Intermediate Language(MSIL)とも呼ばれ、単純にILと略されることもあります。中間言語を含む共通言語基盤CLI(Common Language Infrastructure)は、既にEcma-335やISO/IEC 23271、JIS X3016などで標準化されているため、Microsoftによって独占されている技術ではありません。
ILDASMで表示されるテキストは、中間言語に直接対応しているCILアセンブリ言語(CIL Assembly Language)で書かれたコードです。このアセンブリ言語を理解することで、コンパイラが生成した中間言語を読むことができるようになり、高水準言語で記述したコードが、どのような中間言語に変換されたのかを理解できるようになります。またCILを理解することは.NET Frameworkの本質にも迫ることになります。
.NET Framework SDKには、ILDASMに対応するアセンブラILASMを提供しています。ILASMは、テキストで書かれたCILコードをアセンブルして.NET Frameworkアプリケーションを生成します。高水準言語のコンパイラと同じように、実行可能ファイルやDLLファイルを生成することができ、開発に実用することも可能です。
CILアセンブリ言語は中間言語に直接対応する唯一の言語です。高水準言語では、言語仕様による多くの制約を受けますが、CILアセンブリ言語には制約はありません。.NET Frameworkでできるすべてのことを表現できる唯一の言語であり、アセンブリを理解することで、動的なコード生成など、.NET Frameworkアプリケーションでの応用も広がります。
アセンブリ言語を使った開発方法
.NET Framework SDKには、CILアセンブリ言語で書かれたソースコードをアセンブルするILASMというツールが含まれています。最新のILASMは、.NET Framework 2.0に含まれているものです。.NET Framework 3.5では、言語仕様に変更が加えられているC#やVisual Basicのコンパイラが含まれていますが、CILそのものに変更はないためILASMは含まれていません。Windows Vistaへのデフォルトのインストール設定では、以下のフォルダに格納されています。
- C:\Windows\Microsoft.NET\Framework\v2.0.50727
このフォルダ内にあるILASM.EXEがアセンブラです。使い方は、CSC.EXEなどと同じように、コマンド名に続いてアセンブルするソースファイルを指定します。必要に応じてオプションを指定することもできますが、詳細はVisual Studioに付属しているドキュメントやILASMのヘルプを参照してください。
>ilasm ソースファイル名
CILアセンブリ言語のソースファイルは、通常はilという拡張子のテキストファイルです。後は、他の高水準言語と同じように、テキストファイルにコードを書いてILASMでアセンブルし、実行可能ファイルやDLLファイルを生成できます。
簡単なサンプル
まずは、何もしないプログラムを作成して、ILASMを使って実行ファイルを生成できるかどうかを試してみましょう。「il」という拡張子のテキストファイルを用意してください。
ソースファイルに記述するコードは、当然CILアセンブリ言語の構文に従うものでなければなりません。トークンの概念などは、他の高水準言語と大きな違いはありません。トークンは、ホワイトスペースや記号で区切られるため、トークンの並びが適切で、構文に従っているのであれば改行やタブを自由に挿入できます。
細かい構文については後述するので、この場ではエントリーポイントとなるメソッドを用意します。C#言語におけるMain()メソッドに相当するものです。エントリーポイントがなければEXEファイルを生成できずエラーとなります。
Sample01 .method static void Main() cil managed { .entrypoint ret }
Sample01は、なにもせずに制御を返すだけのプログラムです。このソースファイルをILASMに渡すと、正しくアセンブルが行われ実行ファイルが出力されることを確認できます。実行すると、なにもせずに終了することを期待してしまいますが、結果は例外が発生して終了します。
>test ハンドルされていない例外: System.BadImageFormatException: ファイル またはアセンブリ 'test.exe'、またはその依存関係の 1 つが読み込め ませんでした。モジュールはアセンブリ マニフェストを含んでいなければ なりません。 ファイル名 'test.exe' です。
これは、上記のコードがアセンブリマニフェストを含んでいないために発生する例外です。高水準言語であれば、コンパイラや開発環境が自動的に設定してくれていた項目ですが、CILアセンブリ言語では、マニフェストを明示的に定義しなければなりません。アセンブリを定義するには、次のような行を加えます。
.assembly アセンブリ名 { アセンブリ情報... }
アセンブリ名には、このマニフェストに含めるアセンブリ名を指定します。その後、{ }内に任意のアセンブリ情報を指定できます。マニフェストには、バージョン情報やカルチャなどを設定できますが必須ではありません。この場では空の{ }を指定します。testというアセンブリ名でマニフェストを作成するには、次のようになるでしょう。
.assembly test { }
.NET Frameworkで定められているアセンブリで必須なのはマニフェストだけです。そのため、型メタデータやコードが無くてもアセンブリを作成することは可能です。実行可能ファイルの作成には、エントリーポイントが必須となるためメソッドが必要ですが、DLLファイルの生成であれば、上記のような空のマニフェストの宣言だけでも正しくアセンブルできます。.assemblyは、グローバルなスコープ上であればどこでも記述できますが、同一のアセンブリ内に複数のマニフェストを含めることはできません。通常は、ファイルの先頭に記述することになるでしょう。
.assembly test { } .method static void Main() cil managed { .entrypoint ret }
Sample02は、マニフェストを含む何もしないエントリーポイントMain()メソッドを定義するコードです。アセンブルして生成されたファイルを実行してください。今度はマニフェストを含んでいるため、プログラムは何もせずに正しく終了します。
クラスを持たないMain()メソッドが、グローバルなスコープに配置されていることにC#プログラマは驚くかもしれませんが、C++/CLIでは可能だったことです。同じように、CILアセンブリ言語ではグローバル変数も使うことができ、言語的な制約を受けません。
また、Main()メソッドのブロック内に.entrypointという記述があることに注目してください。これは、このメソッドがエントリーポイントであることを表しています。ここから想像できるように、CILアセンブリ言語ではエントリーポイントのメソッド名は任意です。Main()メソッドである必要はなく、Start()メソッドでもInit()メソッドでも構いません。
Hello World
次に、いわゆる"Hello world"サンプルを作りましょう。文字列をコンソールに表示する、最も簡単なプログラムです。.NET Frameworkアプリケーション開発者であれば、方法は既に理解しています。System.Console.WriteLine()メソッドに、文字列オブジェクトを渡せばよいのです。
メソッド本体に記述するコードは、計算やメソッドの呼び出しなど、共通言語ランタイムが実行する実質的な命令となります。命令は、ネイティブのアセンブリ言語の仕組みと同じように、命令を表す整数、すなわちオペコードに直接対応しています。メソッドの呼び出しにはcall命令を使います。
call method
ただし、アセンブリ言語では高水準言語のようにパラメータを渡すことはできません。メソッドを呼び出す前にパラメータとして渡す値を読み込む必要があります。文字列をWriteLine()メソッドに渡すために、ldstr命令を使って文字列リテラルを事前に読み込みます。詳しくは後述しますが、データはスタックに積み上げられます。
ldstr string
stringには、読み込む文字列リテラルを指定します。メソッドの呼び出しやパラメータの受け渡しの詳細は、割愛させていただきます。この場では、ldstr命令で文字列を読み込み、その後にcall命令でWriteLine()メソッドを呼び出すことで、直前に読み込んだ文字列がパラメータとして渡されるという流れを理解していただければ十分でしょう。
.assembly extern mscorlib { } .assembly test { } .method static void Main() cil managed { .entrypoint ldstr "さすが ILAsm!C# にはできないことを平然とやってのける" call void [mscorlib]System.Console::WriteLine(string) ret }
さすが ILAsm!C# にはできないことを平然とやってのける
Sample03は、ldstr命令で文字列リテラルを読み込み、文字列オブジェクトをパラメータとして受け取るWriteLine()メソッドを呼び出しています。CILアセンブリ言語でも、識別子の考え方は高水準言語と同じです。識別子から、対象のフィールドやメソッドなどのメンバにアクセスできます。System.Consoleクラスはmscorlibアセンブリ内にあるので、ファイルの先頭でmscorlibアセンブリを参照することを宣言しています。