KEN's .NET

[IL2] 計算してみよう 〜足し算・引き算〜

ホーム > KEN's .NET > [IL2] 計算してみよう 〜足し算・引き算〜

ここでは.NET Frameworkのアセンブリ言語MSIL(Microsoft Intermediate Language)を使ったプログラミングを紹介します。 読者は.NET プログラミング経験があることを想定しています。

1. はじめに

今回は足す(+)、引く(-)の計算をしてみます。

2. スタック遷移図

.NET仮想マシンはスタックマシンであることを前回説明しました。 今後の説明の様々な場面で、IL命令によりスタックがどのように変化するかを説明する必要があります。 そのとき、スタック遷移図(state transition diagram)を使用しますので、この図の見方について説明します。

スタック遷移図は命令の実行前後のスタックの状態を示します。 以下はadd命令(2つの値の加算を行う命令)に対するスタック遷移図の例です。

     …, value1, value2 → …, result

スタック遷移図は、→の左側と右側に分かれ、左側が命令実行前の状態、右側が命令実行後の状態を表します。 →の左側は、add命令実行前には、少なくとも2つの要素がスタックに積み上げられている必要があることを表します。 →の右側は、add命令実行後の実行後のスタックの状態が、2つの要素を消費して、結果としてresultの値に変わることを意味します。 そして、この定義では、スタックの最上段の値(一番最後にpushされた値)をvalue2と呼んでいます。 最上段の次の段の値(value2の前にpushされた値)をvalue1と呼んでいます。前回のスタックの説明では値を縦に積むイメージで説明しましたが、 スタック遷移図では、スタックを横置きしたイメージで、→を挟んだ左側、右側のそれぞれの右端がスタックの上部を意味します。

add命令は、スタックからvalue2、value1の順に値を取り出して、スタックにresultの値を格納します。 つまり、add命令の実行前には、スタック上に2つの値をpushして置く必要があり、 add命令の実行後にはそれら2つの値はpopされ、代わりに計算結果resultがpushされるということを意味します。

…はスタックに既に0個以上の要素が置かれていることを示します。…の記述がある場合はあまり気にする必要はなく、 どちらかというと…がない場合にそこには何の要素もあってはいけないことを表します。

3. 計算に使う命令

今回の計算に使う命令の一覧表を掲載します。 下記の表の「命令書式」欄はその命令の使い方を表します。今回の命令はそれ単独で使用するので、 「命令」欄とたまたま同じですが、例えば、ldc.i4命令(32ビット整数の定数をスタックにロードする命令)では、 ldc.i4 numなどのような命令書式になり、numには32ビット整数を指定するといった内容になります。

命令命令書式説明スタック遷移図例外
addadd2つの値を加算し、結果をスタックに置く。…, value1, value2 → …, resultなし
add.ovfadd.ovfオーバーフロー検査を伴う符号付き整数値の加算。…, value1, value2 → …, resultSystem.OverflowException:結果がその型で表現できない場合。
add.ovf.unadd.ovf.unオーバーフロー検査を伴う符号なし整数値の加算。…, value1, value2 → …, resultSystem.OverflowException:結果がその型で表現できない場合。
subsubvalue1からvalue2を減算し、結果を返します。…, value1, value2 → …, resultなし
※ subにもsub.ovf/sub.ovf.unの命令がありますが、考え方はaddの場合と同じため今回は説明しませんので、 一覧表にも記載していません。

4. 足し算・引き算を行う

まずは単純な足し算と引き算からです。図1のサンプルプログラムをご覧ください。

図1 足し算/引き算を行うサンプルプログラム
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.assembly calcAddSub {}
.method public static void main()
{
    .maxstack 2
    .entrypoint
    // 足し算のサンプル
    ldc.i4 5
    ldc.i4 9
    add
    call void [mscorlib]System.Console::WriteLine(int32)    // 5+9 → 14
    // 引き算のサンプル
    ldc.i4 10
    ldc.i4 9
    sub
    call void [mscorlib]System.Console::WriteLine(int32)    // 10-9 → 1
    // 足し算と引き算を連続して行うサンプル
    ldc.i4 5
    ldc.i4 9
    add
    ldc.i4 6
    sub
    call void [mscorlib]System.Console::WriteLine(int32)    // 5+9-6 → 8
    ret
}

このILコードをcalcAddSub.ilに保存して、アセンブルと実行をしてみてください。 出力される結果はコメントに記載していますので、参考にしてください。 次はILコードを見ていきましょう。ポイントは以下の通りです。

  • 4行目の.maxstackでそのメソッドで使用するスタックの最大サイズを指定し、スタック領域を確保します。 このソースでは計算対象の2値をスタックに置いた状態が最もスタックを使用している状態のため、2を指定しています。 1を指定した場合、アセンブルはできますが、スタックの領域が足りないので実行時にInvalidProgramExceptionが発生します。 また、3以上を指定する場合、動作は問題ありませんが、余分なスタックが確保されるので無駄があります。 ちなみにこの指定を省略した場合は8を指定したものとして扱われます。
  • 7〜10行目で足し算を行い、結果を表示します。add命令のスタック遷移図通りにまずは計算対象の2つの値をロードします。 その上でadd命令を実行します。計算対象の2値はなくなり、計算結果がスタックに置かれます。 System.Console::WriteLineメソッドはスタックに置かれたこの結果を引数として呼ばれ、足し算結果が表示されます。
  • 12〜15行目で引き算を行い、結果を表示します。先ほどのaddの場合とまったく同じ流れです。 唯一の注意点は、先にスタックにロードした値(value1)から後にスタックにロードした値(value2)が引かれる点です。
  • 17〜22行目は応用問題ですが、説明は不要でしょう。よくわからないなと思ったら、プログラムを1行ずつ読み進める際に スタックの状態を紙などに書いて追ってみてください。スタックの状態さえ把握しておけば難しくないはずです。

5. add.ovf/add.ovf.un命令

add命令に似た命令としてadd.ovf/add.ovf.un命令があります。 目的はどちらも足し算(加算)を行うためのものですが、以下の点でaddと異なります。

  • add.ovf(add overflowの意) … add命令ではオーバーフロー例外は発生しませんが、 add.ovfでは加算結果が32ビット符号付き整数の範囲(-2,147,483,648〜+2,147,483,647)を超える場合、オーバーフロー例外が発生します。
  • add.ovf.un(add overflow unsignedの意) … add.ovf同様に加算結果が扱える範囲を超えた場合に オーバーフロー例外が発生しますが、その扱える範囲が32ビット符号無し整数の範囲(0〜4,294,967,295)となります。
この違いを図2のプログラムで検証します。 このILコードをcalcAddOvf.ilに保存して、アセンブルと実行をしてみてください。

図2 add.ovf/add.ovf.un命令を検証するプログラム
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.assembly calcAddOvf {}
.method public static void main()
{
    .maxstack 2
    .entrypoint
    // (1)符号付き整数の足し算のオーバーフローチェックなしの場合のサンプル
    ldc.i4 0x7FFFFFFF        // int32の正の最大値
    ldc.i4 0x7FFFFFFF        // int32の正の最大値
    add
    call void [mscorlib]System.Console::WriteLine(int32)           // -2 = 0xFFFFFFFE
    // (2)符号なし整数の足し算のオーバーフローチェックありの場合のサンプル
    ldc.i4 0xFFFF0000        // 符号なしとして扱うので4294901760を意図
    ldc.i4 0x0000FFFF        // 符号なしとして扱うので65535を意図
    add.ovf.un
    call void [mscorlib]System.Console::WriteLine(unsigned int32)  // 4294967295 = 0xFFFFFFFF
    // (3)符号付き整数の足し算のオーバーフローチェックありの場合のサンプル
    ldc.i4 0x7FFFFFFF        // int32の正の最大値
    ldc.i4 0x00000001        // 1
    add.ovf
    call void [mscorlib]System.Console::WriteLine(int32)    // オーバーフローのため未実行
    ret
}

-2
4294967295

ハンドルされていない例外 : System.OverflowException: 演算操作の結果オーバーフロ
ーが発生しました。
   at main()
図3 add.ovf/add.ovf.un命令を検証するプログラムの実行結果

図2のプログラムの記述方法で0x7FFFFFFFのような表現は16進表記で、C#と同じ記法(VBでは&H7FFFFFFF)です。 32ビットの正の最大数を10進表記で表すと記載ミスしやすいので今回はこのように記述しました。 図2の(1)のパターンの0x7FFFFFFF + 0x7FFFFFFFを手計算で計算すると以下のようになります。

16進表記:0x7FFFFFFF
 2進表記:01111111 11111111 11111111 11111111
    ↓
  01111111 11111111 11111111 11111111
+ 01111111 11111111 11111111 11111111
-------------------------------------
  11111111 11111111 11111111 11111110
    ↓
16進表記:0xFFFFFFFE
 2進表記:11111111 11111111 11111111 11111110
ここで計算結果の0xFFFFFFFEを10進表記で取り扱う場合に符号付き整数か符号無し整数かで異なる結果になります。 これは符号付き整数は最上位1ビット(上記の2進表記での左端の1ビット)は符号を表すビットとして扱うためです。 最上位1ビットが0であれば正の数、1であれば負の数を意味します。 計算結果の0xFFFFFFFEは最上位ビットが1のため、符号付き整数では-2を意味するので、 正の数同士を足し算したにも関わらず、答えは負の数になります。 これはadd命令の動作がおかしいのではありません。このような動作の命令なのです。 しかし、たいていのプログラムでは、図2の(1)のadd命令を使った例のようなパターンは意図しないものであるはずです。 10進数として意味のある計算を行うのであれば、このことを考慮して図2の(3)のようにオーバーフロー例外の起きるadd.ovf命令を使います。

add.ovfは正の整数としては0x80000000以上の値をオーバーフロー例外としますが、 符号無し整数同士の足し算を考えた場合はこれはオーバーフローではありませんので、 符号無し整数同士の足し算を行うための命令としてadd.ovf.unがあります。つまりオーバーフローにするかどうかの基準値が違うということです。 図2の(2)の例ではちょうど符合無し整数の最大値になる計算のため例外は発生しませんが、 試しに0xFFFF0000を0xFFFF0001などに修正して実行してもらえば、例外が発生することを確認できます。

6. 学んだこと

  • スタック遷移図の読み方
  • add/add.ovf/add.ovf.un/sub命令
  • .maxstackでメソッドで使用するスタック領域のサイズを指定するらしい
  • 0x7FFFFFFFのような表記で16進表記を意味する(C#と同じ!)
  • 符号付き整数と符号無し整数

A. サンプルダウンロード


ホーム > KEN's .NET > [IL2] 計算してみよう 〜足し算・引き算〜

[e-mail] yone_ken00@hotmail.com