計算機をつくってみようの続きです。内容は独立しているのでこの演習から始めても問題ありませんが、計算機を自分で作ってからのほうが理解が捗ると思われます。
MSILはCIL(Common Intermediate Language)のMS実装です。CILはdotnetのruntimeが実行できる中間言語です。
C#やF#、Visual Basicをコンパイルするとdllやexeができます。これらの成果物は中間言語であるMSILやCILのバイナリになっています。
runtimeがこのdllやexeを実行するにアーキテクチャに沿ったNativeコードに変換しアプリケーションが実行されます。
+----+ +----+ +--------+
| C# | | F# | | VB.NET |
+----+ +----+ +--------+
| | |
| | |
+------v------+ +------v------+ +------v------+
| C# Compiler | | F# Compiler | | VB Compiler |
+-------------+ +-------------+ +-------------+
| | |
| | |
| | |
| | |
| | |
| +--------------+ |
+---------> dll or exe +<---------+
| (MSIL / CIL) |
+--------------+
|
|
+-------------------------------------------+
| | |
| +-------------v-------------+ |
| |JIT (Just In Time) Compiler| |
| +---------------------------+ |
| | |
| | |
| +-----v-----+ |
| |Native Code| |
| +-----------+ |
| Runtime |
+-------------------------------------------+
実際にMSILを見てみましょう。まずは対象のexeを用意します。
using System;
namespace ConsoleApp1
{
class Program
{
static void Print(int n)
{
Console.WriteLine("Result {0}", n);
}
static void Main(string[] args)
{
var l = 100;
var r = 1;
var n = l + r;
Print(n);
}
}
}
これをコンパイルし、ConsoleApp1.exeを用意しましょう。
exeはそのままではバイナリ形式なので簡単には理解することができません。ildasmというアプリケーションを使用して逆アセンブルしてみましょう。
ildasmはVisualStudioをインストールした際に入っているので、開発者コンソールから起動してみましょう。
**********************************************************************
** Visual Studio 2019 Developer Command Prompt v16.1.5
** Copyright (c) 2019 Microsoft Corporation
**********************************************************************
C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise>ildasm
ファイルメニュー、開くから先ほどコンパイルしたexeを開きましょう。
exeを開くと次のようなツリーがでたはずです。※下記のツリーはテキストダンプしたものです。ウィンドウでは[xxx]の部分はアイコンになっています。
[MOD] <dll path>
| M A N I F E S T
|___[NSP] ConsoleApp1
|___[CLS] ConsoleApp1.Program
| .class private auto ansi beforefieldinit
|___[MET] .ctor : void()
|___[STM] Main : void(string[])
|___[STM] Print : void(int32)
Label | Description |
---|---|
MOD | Module |
NSP | Namespace |
CLS | Class |
MET | Method |
STM | Static method |
作成したProgram
クラスにスタティックメソッドのMain
とPrint
があることが確認できます。
ツリーでMain
をクリックするとILのウィンドウが開きます。
開いたウィンドウにはMainメソッドのILを逆アセンブルして人間に読みやすいようにして表示されているのでこれを読んでいきましょう。
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// コード サイズ 18 (0x12)
.maxstack 2
.locals init (int32 V_0,
int32 V_1,
int32 V_2)
IL_0000: nop
IL_0001: ldc.i4.s 100
IL_0003: stloc.0
IL_0004: ldc.i4.1
IL_0005: stloc.1
IL_0006: ldloc.0
IL_0007: ldloc.1
IL_0008: add
IL_0009: stloc.2
IL_000a: ldloc.2
IL_000b: call void ConsoleApp1.Program::Print(int32)
IL_0010: nop
IL_0011: ret
} // end of method Program::Main
1行目のメソッド宣言はとばして{}
の中だけを読んでいきましょう。
IL | 説明 |
---|---|
.entrypoint | 実行時の開始時点の表す |
.maxstack 2 | スタックサイズを2に設定する |
.locals init (int32 V_0, int32 V_1, int32 V_2) | int型の変数を3つ用意する |
nop | 何もしない |
ldc.i4.s 100 | スタックに4byteの固定値=100を積む |
stloc.0 | 0番目の変数=V_0にスタックに積まれた値=100を格納する |
ldc.i4.1 | スタックに4byteの固定値=1を積む(ldc.i4.s 1とも書ける) |
stloc.1 | 1番目の変数=V_1にスタックに積まれた値=1を格納する |
ldloc.0 | V_0の値をスタックに積む |
ldloc.1 | V_1の値をスタックに積む |
add | スタックに積まれた値、100と1を加算する |
call void ConsoleApp1.Program::Print(int32) | Printメソッドをコールする |
nop | 何もしない |
ret | 処理を終了する |
「おっ!どこかで見たことあるぞ!?」と感じましたでしょうか。そう、計算機をつくってみようで作成した計算機と同様にMSILもスタックをベースに演算を実行していくのです!
先ほど書いたPrint
メソッドが呼び出されている箇所がありました。今度はPrintメソッドを読んでみましょう。
ツリーでPrint
をクリックするとILのウィンドウが開き、Mainと同様に人間が見やすい逆アセンブルされたコードを見ることができます。
.method private hidebysig static void Print(int32 n) cil managed
{
// コード サイズ 19 (0x13)
.maxstack 8
IL_0000: nop
IL_0001: ldstr "Result {0}"
IL_0006: ldarg.0
IL_0007: box [System.Runtime]System.Int32
IL_000c: call void [System.Console]System.Console::WriteLine(string,
object)
IL_0011: nop
IL_0012: ret
} // end of method Program::Print
IL | 説明 |
---|---|
.maxstack 8 | スタックサイズを8に設定する |
nop | 何もしない |
ldstr "Result {0}" | スタックに文字列Result {0} の参照(アドレス)が積まれる |
ldarg.0 | 0番目の引数の値をスタックに積む |
box | スタックに積まれた0番目の引数の値をBox化して参照を積む |
call void [System.Console]System.Console::WriteLine(string,object) | WriteLineメソッドををコールする |
nop | 何もしない |
ret | 呼び出し元(Main)に処理をもどす |
大体処理は読み解けたでしょうか。ここではBox化という新たな言葉が出てきました。次はBox化について詳しく見ていきましょう。
先ほどのPrintメソッドを読み進めているうちにbox命令があらわれました。この命令は値を参照に変換する命令です。この変換をBox化と呼びます。このBox化について詳しく見ていきましょう。
値と参照
計算機をつくってみようで作成した計算機は、すべてのデータはint型でした。しかし、私たちは.NETにそれ以外の型があり、必ずしも4bytesに収まるものではないことを知っています。
例えば、Print
メソッドで出てきた文字列Result {0}
を考えてみましょう。
簡単にするために、ASCIIコードで考えると文字列Result {0}
は10bytesです。ldstr
命令では、この10bytesをそのままスタックに積んでいるのでしょうか? 答えはNOです。仮にそのまま積んでしまったら、popする際に文字列データはバラバラになってしまいます。
それでは、ldstr
命令の実行時にスタックには何を積んでいるのかというと、
文字列Result {0}
のデータはスタックとは関係ない全く別の領域に配置されており、そこの場所を表す値=参照をスタックに積んでいます。
この仕様は、文字列だけではありません。classから生成したインスタンスや配列などの固定長でないものは参照で取り扱われます。
値と参照については以下のページに詳しく記載されているので時間があれば読んでみましょう。
値型と参照型 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
Box化
さて、本題のbox化ですがbox化は値を参照に変換するということです。なぜ、そのようなことをする必要があるのでしょうか? Print
メソッドのSystem.Console.WriteLine
を呼び出している箇所のILを確認してみましょう。
IL_0001: ldstr "Result {0}"
IL_0006: ldarg.0
IL_0007: box [System.Runtime]System.Int32
IL_000c: call void [System.Console]System.Console::WriteLine(string,
object)
System.Console::WriteLineの引数は2つで、その引数をIL_0001
,IL_0006
,IL_0007
で準備しています。box化しているのは2つ目引数です。2つ目の引数の型をIL_000c
で確認すると、object
型であることがわかります。C#ではobject
はすべてのクラスが継承しているクラスですので、2つ目の引数で要求しているのは参照であることがここからわかります。しかし、我々がC#のコードで引数に渡しているのは、int
型であり、値です。この値そのままでは、メソッドの要求を満たすことができません。これが、IL_0007
でbox化している理由です。
クラス、構造体、メソッド、スタティックメソッドを使用するC#コードを書いてそのILを読んでみよう。