制御
次に、簡単な制御方法について説明します。プログラムの流れ制御は、高水準言語のような構造的なものではないので、命令単位に自由にジャンプできます。いわゆるif文のような分岐も可能ですし、goto文のような無条件ジャンプも可能です。
無条件ジャンプは、強制的に実行するコードを指定の位置に移動するbr命令を使います。
br target
targetには、移動先のコードのアドレスを指定します。実際には、メソッド内のコードのオフセットを表す4バイトの整数が保存されますが、アセンブリ言語内では命令に名前を付けて、識別子で指定できます。命令には、コードラベルと呼ばれる名前を付けることができます。コードラベルは次のように指定します。
Id:
このコードラベルの直後が、移動先の対象命令となります。コードラベルのIdに指定した識別子を、br命令のtargetに設定することで、強制的にラベルの命令まで移動します。この命令を使うことで、同一メソッド内の任意の命令にジャンプできます。
.assembly extern mscorlib { } .assembly test { } .method static void Main() cil managed { .entrypoint ldstr "さぁ、始まるザマスよ" call void [mscorlib]System.Console::WriteLine(string) br target ldstr "行くでガンス" call void [mscorlib]System.Console::WriteLine(string) target: ldstr "ふんがー" call void [mscorlib]System.Console::WriteLine(string) ret }
さぁ、始まるザマスよ ふんがー
Sample05は、メソッドの途中でbr命令を使って、途中のいくつかの命令を省略するプログラムです。コード内では3回WriteLine()メソッドの呼び出しが行われていますが、中間の呼び出しは実行されません。br命令が実行された時点で、制御はtargetラベルのldstrに移動します。
条件分岐には、計算と同じようにスタックに読み込まれている値が使われます。分岐の方法は、値が0以外のtrueを表す場合に実行されるbrtrue命令と、値が0、すなわちfalseやnullの場合に実行されるbrfalse命令があります。
brtrue target
書式はbr命令と同じです。この命令を実行すると、スタックに読み込まれている値がポップされ、その値が0以外であればtargetに分岐します。nullでなければ分岐するという意味の別名でbrinst命令も用意されています。brtrueもbrinstもオペコードは同じなので、単純な別名です。どちらを使っても構いませんが、通常は調べる対象が値型であればbrtrue、参照型であればbrinstを使った方が分かりやすいでしょう。
値が0であれば分岐するbrfalse命令も、仕組みは同じです。
brfalse target
やはり、スタックに読み込まれている値がポップされ、その値が0であればtargetに分岐します。同一の意味をもつ別名の命令でbrnullおよびbrzeroも用意されていますが、やはりオペコードは同じです。
brtrueまたはbrfalseいずれかの命令を使うことで、高水準言語におけるif-else文の処理が実現できます。
.assembly extern mscorlib { } .assembly test { } .method static void Main() cil managed { .entrypoint ldc.i4.1 brfalse else ldstr "スタックは true です" call void [mscorlib]System.Console::WriteLine(string) br endif else: ldstr "スタックは false です" call void [mscorlib]System.Console::WriteLine(string) endif: ldstr "分岐処理を終了します" call void [mscorlib]System.Console::WriteLine(string) ret }
スタックは true です 分岐処理を終了します
Sample06は、最初のldc.i4.1命令でスタックに読み込んだ値を使って分岐処理を行い、trueかfalseかを表す文字列を出力します。命令にはbrfalseを使っているので、スタックの値がtrueであればそのまま逐次に実行され、そうでなければelseラベルまでジャンプします。brfalseでジャンプしなかった場合、そのままコードが続けて実行されます。elseラベル以降がそのまま実行されないように、br命令でendifまでジャンプしています。
ちなみに、.NET Framework SDK付属のC#コンパイラでbool型の変数をif文に与えた場合、変数の値と0をスタックに読み込んだあと、値が等しいかどうかを調べるceq命令を使っています。その後brtrue命令で変数の値が0、すなわちfalseであればelse部分にジャンプし、そうでなければ実行を続けるという流れになっていました。
本稿では解説していないため割愛しますが、変数を使うことで反復処理も同じ原理で実現できます。変数に繰り返す回数を代入し、変数の値をスタックに読み込んで0かどうかを調べ、0でなければ処理を実行して値をデクリメントします。最後に、処理の冒頭にジャンプすることで、変数の値が0になるまで処理が繰り返されます。
最後に
さて、本稿ではアセンブリ言語の基本的な構造と命令について説明しました。本稿の内容に加えて、クラスやメソッドなどを宣言するディレクティブを使うことで、他の高水準言語と同じように、構造的にコンポーネントを開発できます。ディレクティブについては割愛しましたが、その多くはVisual BasicやC#の経験があれば、直観的に理解できるものでしょう。ILDASMを使って逆アセンブルしたコードを解析して、コンパイラがどのような中間言語を生成しているのかを調べるのも面白いと思います。
アセンブリ言語を実践の開発現場で実用することは、生産性や保守性の面で困難かもしれませんが、これを理解することで.NET Frameworkの本質が見えてきます。もし、Visual BasicチームとC#チームが対立し、どうにもプロジェクトがまとまらないときは、アセンブリ言語を叩きこむことで仲良くなるかもしれません。少数派のC++/CLIチームが親の敵のようにマネージテンプレートを使って我が道を突き進んでいる時も、逆アセンブルすることで、どのようなコードが生成されているのか調べられます。
どちらにしても、アセンブリ言語を理解することで、こういった小回りが利くことは多少なりのメリットになるはずです。さらに、特殊な開発支援ツールや、実行時の型解析、ロジックの生成などにも、CILの知識が必要になります。動的な中間言語の生成は、間もなく登場する予定の動的言語ランタイムや.NET Framework 3.5で追加された式木の基礎にもなっている重要なテクニックです。
アイデア次第で面白いプログラムが書けるかもしれません。ぜひ試してみてください。
参考
- 『Inside Microsoft .NET IL Assembler』(Serge Lidin 著、Microsoft Pr、2002年2月)
- Standard ECMA-335 Common Language Infrastructure (CLI)(Ecma International)