雑記 - otherwise

最近はDQ10しかやっていないダメ技術者がちまちまと綴る雑記帳

式のトレース (3)

せっかくなので、最初の課題の MSIL を読み解いてみたいと思います。
まずは、検証に使った C# コード(該当メソッドのみ)はこちら。

static void Main() {
    var i = 0;
    i *= ~i & 2 | 1 * (++i) - -(i--) ^ i;
    Console.WriteLine("i = {0}", i);
}

続いて .NET Reflector で該当プログラムの MSIL を取得した結果。
( VS で MSIL を見る方法を知らないので。。。 ^^;; )

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 6
    .locals init (
        [0] int32 num)
    L_0000: nop 
    L_0001: ldc.i4.0 
    L_0002: stloc.0 
    L_0003: ldloc.0 
    L_0004: ldloc.0 
    L_0005: not 
    L_0006: ldc.i4.2 
    L_0007: and 
    L_0008: ldloc.0 
    L_0009: ldc.i4.1 
    L_000a: add 
    L_000b: dup 
    L_000c: stloc.0 
    L_000d: ldloc.0 
    L_000e: dup 
    L_000f: ldc.i4.1 
    L_0010: sub 
    L_0011: stloc.0 
    L_0012: neg 
    L_0013: sub 
    L_0014: ldloc.0 
    L_0015: xor 
    L_0016: or 
    L_0017: mul 
    L_0018: stloc.0 
    L_0019: ldstr "i = {0}"
    L_001e: ldloc.0 
    L_001f: box int32
    L_0024: call void [mscorlib]System.Console::WriteLine(string, object)
    L_0029: nop 
    L_002a: ret 
}

このままだと判りにくいので、各行毎に、変数とスタック領域の値の遷移を表にしてみます。
※ 0000 は初期化 (nop) なので省略、 0019 以降は出力処理なので省略してあります。
※「 - 」は何もセットされていない状態を示します。(「 0 」がセットされている状態と明確に区別したかったので分けました)

Line i stack1 stack2 stack3 stack4 stack5 stack6
0001 - 0 - - - - -
0002 0 - - - - - -
0003 0 0 - - - - -
0004 0 0 0 - - - -
0005 0 0 -1 - - - -
0006 0 0 -1 2 - - -
0007 0 0 2 - - - -
0008 0 0 2 0 - - -
0009 0 0 2 0 1 - -
000a 0 0 2 1 - - -
000b 0 0 2 1 1 - -
000c 1 0 2 1 - - -
000d 1 0 2 1 1 - -
000e 1 0 2 1 1 1 -
000f 1 0 2 1 1 1 1
0010 1 0 2 1 1 0 -
0011 0 0 2 1 1 - -
0012 0 0 2 1 -1 - -
0013 0 0 2 2 - - -
0014 0 0 2 2 0 - -
0015 0 0 2 2 - - -
0016 0 0 2 - - - -
0017 0 0 - - - - -
0018 0 - - - - - -

……相変わらず判りにくいか。 ^^;
それでは上から読み解いてみます。

0001 〜 0002

0001 でスタック上に「 0 」を格納して、 0002 でローカル変数 (i) に代入しています。
(「 var i = 0 」の部分)

0003

最後の復号代入演算に使用するために i の値をスタック上にコピーします。
# ってことで、「最後の、複合代入演算子によって、後の演算結果に関係なくゼロが乗算の左辺になる」訳ですね。
……なので、これ以降のロジックは本質的に無意味なんですが、折角なので右辺の演算も見てみます。

0004 〜 0005

0004 で i の値をスタック上にコピーして、 0005 で NOT 演算を行っています。 (~i)

0006 〜 0007

0006 でスタック上に「 2 」を格納して、 0007 で AND 演算を行っています。 (&)

0008 〜 000c

0008 で i の値をスタック上にコピー、 0009 でスタック上に「 1 」を格納して、 000a で二数を足しています。
これによってインクリメントを行っています。 (++i)
更に 000b でスタック上の値を複写して、 000c で i に代入しています。
( i への代入前に複写するのは、 i に代入した上で次の計算にその値を利用するため)

000d 〜 0011

000d で i の値をスタック上にコピー、 000e で複写をします。
(ここで複写しているのは、デクリメント前の値を後続の計算で使用するため)
その後、 000f でスタック上に「 1 」を格納して、 0010 で二数を引きます。
これによってデクリメントを行っています。 (i--)
更に 0011 で i に代入しています。

0012

ここで符号反転をしています。 (-)

0013

スタック上の二数を引きます。 (-)

0014 〜 0015

0014 で i の値をスタック上にコピー、 0015 で XOR 演算を行っています。 (^)
※ここで判る様に、 XOR 演算子で使用される i は i-- の結果になります。

0016

OR 演算を行っています。
演算の対象は、 (~i & 2) の結果と ((++i) - -(i--) ^ i) の結果になります。
(~i & 2) は 0004 〜 0007 の結果から 2 、 ((++i) - -(i--) ^ i) の結果は 0008 〜 0015 の結果から 2 になるので、 OR の結果は 2 です。

0017 〜 0018

最後に復号代入演算が行われます。
0017 で 0003 でコピーした i の値と 0016 までの右辺演算結果をかけています。
0003 でコピーした i は 0 だったので、計算結果は 0 です。
そして、 0018 で結果 (0) を i に代入して完了です。
……ふぅ、長かった。