前回は、CIL で型やメンバーがどう表現されるかを見ました。
今回は、CIL の命令をもう少し詳しく読みます。
特に、評価スタック、ローカル変数、引数、this 参照、分岐、ループに注目します。
命令を読む前に
CIL はスタックベースです。
多くの命令は、評価スタックへ値を積む、評価スタックから値を取り出す、結果を再び積む、という動きをします。
代表的な命令をいくつか見ておきます。
値を積む命令
ldc.i4.0
ldc.i4.1
ldc.i4.s 10
ldstr "hello"
ldarg.0
ldloc.0
整数、文字列、引数、ローカル変数などを評価スタックへ積みます。
値を保存する命令
stloc.0
stfld int32 Sample::count
評価スタック上の値を、ローカル変数やフィールドへ保存します。
計算や比較を行う命令
add
sub
mul
div
rem
ceq
cgt
clt
スタック上の値を取り出して計算や比較を行い、結果をスタックへ戻します。
分岐する命令
br.s LABEL
brtrue.s LABEL
brfalse.s LABEL
ラベルへジャンプします。
C# の if や while は、CIL では分岐命令とラベルで表現されます。
最大スタックサイズ
CIL のメソッドには、評価スタックが最大でどれくらい必要かを示す指定があります。
.maxstack 2
これは「このメソッドでは、評価スタックに最大 2 個の値が同時に積まれる」という目安です。
たとえば、次の C# コードを考えます。
public static int Add(int x, int y)
{
return x + y;
}
概念的な CIL です。
.maxstack 2
ldarg.0
ldarg.1
add
ret
ldarg.0 と ldarg.1 で 2 つの値が積まれるため、最大スタックサイズは 2 で足ります。
ローカル変数
C# のローカル変数は、CIL では .locals で宣言されます。
public static int AddWithLocal(int x, int y)
{
int result = x + y;
return result;
}
概念的な CIL です。
.locals init (
[0] int32 result
)
ldarg.0
ldarg.1
add
stloc.0
ldloc.0
ret
流れは次の通りです。
x を積む
y を積む
加算する
結果をローカル変数 0 番へ保存する
ローカル変数 0 番を積む
戻り値として返す
C# では変数名で扱いますが、CIL ではインデックスで扱われます。
引数とローカル変数の対応
引数は ldarg 系の命令で読み込みます。
ldarg.0
ldarg.1
ldarg.2
静的メソッドでは、ldarg.0 が 1 番目の引数です。
インスタンスメソッドでは、ldarg.0 は this を表し、実際の第 1 引数は ldarg.1 になります。
public class Counter
{
private int value;
public void Add(int delta)
{
value += delta;
}
}
概念的な CIL です。
ldarg.0
ldarg.0
ldfld int32 Counter::value
ldarg.1
add
stfld int32 Counter::value
ret
ここで ldarg.0 は this、ldarg.1 は delta です。
隠れた this 参照
インスタンスメソッドには、C# の引数一覧に見えない this が暗黙的に渡されます。
public string GetName()
{
return Name;
}
概念的な CIL です。
ldarg.0
call instance string Person::get_Name()
ret
ldarg.0 で現在のインスタンスを積み、それに対してプロパティ取得メソッドを呼び出しています。
この見方ができると、インスタンスメソッドと静的メソッドの違いがかなり明確になります。
条件分岐
C# の if 文です。
public static string Check(int value)
{
if (value > 0)
{
return "positive";
}
return "zero or negative";
}
概念的な CIL です。
ldarg.0
ldc.i4.0
cgt
brfalse.s ELSE_LABEL
ldstr "positive"
ret
ELSE_LABEL:
ldstr "zero or negative"
ret
cgt は「より大きいか」を比較し、結果をスタックへ積みます。
brfalse.s は、スタック上の値が false 相当なら指定ラベルへジャンプします。
ループ
C# の for 文です。
public static int SumTo(int max)
{
int total = 0;
for (int i = 1; i <= max; i++)
{
total += i;
}
return total;
}
概念的な CIL は、ラベルと分岐で表現されます。
.locals init (
[0] int32 total,
[1] int32 i
)
ldc.i4.0
stloc.0
ldc.i4.1
stloc.1
br.s CHECK
LOOP:
ldloc.0
ldloc.1
add
stloc.0
ldloc.1
ldc.i4.1
add
stloc.1
CHECK:
ldloc.1
ldarg.0
ble.s LOOP
ldloc.0
ret
C# のループ構文は、CIL では「初期化」「条件確認」「本体」「更新」「分岐」の組み合わせになります。
ラベルの役割
ラベルは、分岐命令のジャンプ先です。
br.s CHECK
LOOP:
// ループ本体
CHECK:
// 条件確認
C# の if、for、while、switch などは、CIL 上ではラベルと分岐命令に変換されます。
高水準の構文は違って見えても、低水準では「どこへジャンプするか」が重要になります。
CIL を読むときのコツ
CIL を読むときは、すべての命令を丸暗記しようとしなくて大丈夫です。
まずは次の順番で追うと読みやすくなります。
- メソッドの引数と戻り値を見る
- ローカル変数を見る
- スタックへ値を積む命令を見る
- 計算や比較の命令を見る
- 分岐先のラベルを見る
retまでの流れを追う
特に、スタック上に今どの値が積まれているかを頭の中で追えるようになると、CIL の見通しがよくなります。
まとめ
CIL の命令は、評価スタックを中心に動きます。
引数やローカル変数をスタックへ積み、計算し、結果を保存したり返したりします。
また、C# の制御構文は、CIL ではラベルと分岐命令として表現されます。
次回は、CIL を手で書くのではなく、実行時にアセンブリを生成する System.Reflection.Emit の世界を見ていきます。