たとえば、C言語でif/elseやswitch、for、while/do-while、条件演算("cond ? v1 : v2" 形式の記述)が使用できないとしたら、有用なプログラムを書くことはできるでしょうか?
実行時に与えられるデータに関わらず、常に一定の手順でしか処理できないとしたら、ソフトウェアの有用性は非常に限定されたものになるでしょう。
今回は、アセンブラにおいて“if”を実現するための、条件判定に関して説明します。
状態フラグ
一般的な CPU アーキテクチャでは、演算等の命令を実行した際に、実行結果に応じて状態が変化するフラグを備えています。
この「実行結果に応じて状態が変化するフラグ」(以下「状態フラグ」)は、特殊なレジスタ上の一部のビットが割り当てられていることが多く、このレジスタは多くの場合、“Condition Code Register”(CCR)や“State Register”(SR)等と呼ばれますが、Intel 80x86アーキテクチャの場合はそのものズバリのEFLAGS という名前のレジスタを持っています(“E”は“Extended”から由来するもので、80286以前の16ビット幅のFLAGレジスタに対して32ビット拡張されていることを意味します)。
なお、実行される命令によっては、更新されるフラグが限定される、つまりそれ以外のフラグには影響を及ぼさない、といった挙動もありますので注意が必要です。
「ゼロ」フラグ
実行結果がゼロか否かを保持するフラグビットを一般にゼロフラグ(Zero Flag)と呼びます。Intel 80x86アーキテクチャでは略称のZFで呼びます。
実際にプログラムを動かしてみましょう。
「キャリー」フラグ
符号無し演算における値域超えの有無を保持するフラグビットを一般にキャリーフラグ(Carry Flag)と呼びます。Intel 80x86アーキテクチャでは略称のCFで呼びます。
実際にプログラムを動かしてみましょう。
上記の実行例ではCFとは別に、演算結果に応じてZFも変化しています。
「オーバーフロー」フラグ
一般的なソフトウェアでは、符号付きの数値を表現する際に「2の補数」と呼ばれる方法を用います。
符号有り演算における値域越えの有無を保持するフラグビットを一般にオーバーフローフラグ(Overflow Flag)と呼びます。Intel 80x86アーキテクチャでは略称のOFで呼びます。
実際にプログラムを動かしてみましょう。
符号付き32ビット数の演算とみなした場合には、値域越えとみなされる「0x80000000 - 0x01 = 0x7fffffff」の演算も、符号無し32ビット数の演算とみなした場合では十分に値域の範囲であるため、桁上がりないし桁借りでセットされるCFは変化しません。
「符号」フラグ
演算結果を2の補数とみなした際に、最上位ビットを符号ビット(sign bit)と呼びます。
このビット値と同じ値を保持するフラグビットは符号フラグ(Sign Flag)、あるいは「負値の場合に1となる」ことから負値フラグ(Negative Flag)と呼びます。Intel 80x86アーキテクチャでは符号フラグの略称のSFで呼びます。
実際にプログラムを動かしてみましょう。
符号付き演算でのオーバーフローの際には、元の数値と演算結果では符号が反転します。
そこで、先述した OF がセットされている=値域超えが発生した際に、SFの設定状況=演算結果の符合ビットを確認することで、演算結果の値域超えがどの方向で発生したのかを判定することができます。
- 結果が負値(=SFセット)なら、上限を超えた演算による値域超え
- 結果が正値(=SFクリア)なら、下限を下回った演算による値域超え
条件分岐
一般的な CPU アーキテクチャは、現在実行中の命令が格納されているアドレスを保持するレジスタを持っています。
このレジスタは通常プログラムカウンタ(program counter)と呼ばれます。32ビットIntel 80x86アーキテクチャの場合はEIP(Extended Instruction Pointer)レジスタが相当します(“extended”は、16ビット幅しか扱えなかった80286以前のIPレジスタに対して、32ビット化拡張がされている、という意味です)。
EIPレジスタは、命令の実行ごとに次の命令位置、次の命令位置へと順次書き換えられますが、この「次に実行する命令の位置」を強制的に変化させる命令を制御転送(Controll Transfer)命令と呼びます。
制御転送命令には幾つか種類がありますが、通常のプログラムで使用するものは、手続き呼び出し(Procedure Call)に関する命令(※)と、それ以外の分岐(Branch)に関する命令に大別できます。
特に分岐命令には、制御転送の実施の有無を状態フラグの状況に応じて変化させる条件分岐(Conditional Branch)命令と、常に分岐を実施する無条件分岐命令があります。
Intel 80x86アーキテクチャの条件分岐命令はJcc
という形式で表記され、cc
部分には分岐を実施する際の条件名称が指定されます(無条件分岐命令はJMP
と記述します)。
主要な条件名称を以下に示します。
表1 条件名称
名称 | 条件 | フラグ状態 |
e (Equal) | 等しい | ZF |
ne (Not Equal) | 等しくない | !ZF |
g (Grater) | 比較値より大きい | !ZF && (SF == OF) |
le (Less or Equal) | 比較値以下 | ZF || (SF != OF) |
ge (Grater or Equal) | 比較値以上 | SF == OF |
l (Less) | 比較値未満 | SF != OF |
a (Above) | (符号無し)比較値より大きい | !CF && !ZF |
na (Not Above) | (符号無し)比較値以下 | CF || ZF |
cc (Carry Clear) | (符号無し)比較値以上 | !CF |
cs (Carry Set) | (符号無し)比較値未満 | CF |
「フラグ状態」は、「条件」欄における大小関係が成立する際に、「検証対象の値」から「比較値」を減算した際のフラグ状態を示しています。
符号無し数の比較や、「等しい」「等しくない」を理解するのは比較的簡単だと思いますが、符号付き数の比較に関するSF/OFの条件は少々面倒かもしれません。
“SF == OF”が成立するのは
- 減算結果が 0 ないし正の値(SF == 0 && OF == 0)
- 演算結果が符号付き数の上限超え(SF == 1 && OF == 1)
のいずれかの場合です(「上限超え」と判断する理由は、SFでの説明をご覧ください)。
減算の際に後者の条件が成立するのは、「検証対象の値」が正の値で、かつ「比較値」が負の値の場合に限定されますから、以上を総合すると、“SF == OF”ということは「検証対象の値」≧「比較値」ということに他ならないことがわかります(“SF != OF”ならその逆)。
なお、Intel 80x86アーキテクチャの状態フラグは、加減算のような(整数)演算命令の実行以外にも、比較(compare)命令CMP
によって更新されます。
結果をレジスタなりメモリに格納する演算命令と違い、比較命令は状態フラグ更新以外の改変副作用が無いため、条件分岐命令のための状態フラグ更新には、比較命令を使用するケースの方が一般的と思われます。
論理積/論理和
実際の条件分岐で指定される条件は、必ずしも前節で述べたような単純な形式ではありません。
たとえば、以下のような条件判定の実装は珍しいものではありません:
上記のような論理積(logical multiplication)を実現する場合、Cコンパイラが生成するアセンブラプログラムは以下のようになります。
また
上記の様な論理和(logical addition)を実現する場合、Cコンパイラが生成するアセンブラプログラムは以下のようになります。
以上のことを踏まえた上でC言語の言語仕様を読めば
- 論理積/論理和による結合は、常に全ての条件が確認されるわけではない
- 論理積による結合は、成立しない条件があった時点で中断され得る
- 論理積による結合は、成立する条件があった時点で中断され得る
という仕様も理解しやすいのではないでしょうか?
分岐以外の状態フラグ参照
条件分岐命令以外の状態フラグ使用の例として、多倍長演算の例を説明したいと思います。
たとえば32ビットアーキテクチャのCPUの場合、加減算は32ビット単位で実施されます。
しかし、64ビットや128ビットといった、より大きな値域での演算が必要な場合はどうすれば良いのでしょうか?
ありがたいことに、このような状況で使用するための拡張加減算とでも言うべき命令が、多くの CPU アーキテクチャでサポートされています。
Intel 80x86アーキテクチャではADC
(ADd with Carry)およびSBB
(SuBtract with Borrow)命令がそれに相当します。
以下は、領域aおよびbに格納された4倍長(128ビット)データの加算を行なうプログラムです(結果は領域bに格納)。
それでは実際に4倍長の加算を行ってみましょう。
まずは桁上がりが発生しないケースとして、0x12345678.12345678.12345678.12345678 同士の加算を見てみましょう(見やすいように、32ビットごとに“.”で区切りました)。
この実行例で行っているようなGDBの“set var
”によるメモリ領域の書き換えは、プログラムの実行を開始(run
)するまでは実施できませんので注意してください。
桁上がりが無い場合は、複数桁の筆算を行うのと同じ要領で、各桁(この場合は32ビット=4バイト)ごとの加算が実施されるだけです。このケースは簡単ですね。
次に32ビット幅間での桁上がりが発生する0x00000001.fffffffd.fffffffe.ffffffffと0x00000004.00000003.00000002.00000001の加算を見てみましょう。
以下の実行例では、初期値の設定手順は省略しています。また、複数の命令を一括して実行するために、実行命令数を指定するための引数をstepi
コマンドに与えています。
桁上がりが次々と伝播していく様子がわかるのではないでしょうか?