演算命令
演算命令の一覧は、汎用命令 (2) でご覧ください。
加算と減算は、基本的に符号付き/符号なしの数値を区別しません (結果としてはどちらも同じ値になるため)。
結果の値がどうなったかは、rFLAGS レジスタのステータスフラグで判断できます。
加算と減算は、基本的に符号付き/符号なしの数値を区別しません (結果としてはどちらも同じ値になるため)。
結果の値がどうなったかは、rFLAGS レジスタのステータスフラグで判断できます。
加算のサンプルコード
汎用の演算命令を実行すると、結果がどうなったかによって、rFLAGS レジスタのステータスフラグが、セットまたはクリアされます。
ADD 命令で値を加算した場合、どのようなフラグがセットされるかを、実際に実行して確認してみます。
<test2.c>
<16_add.asm>
C ソースの方は、testfunc 関数を呼び出すだけで、何もしません。
アセンブラコードでは、AL レジスタを使って、8bit 整数の加算を行います。
※フラグレジスタの値は、GDB のデバッガで確認するため、NASM と C コンパイラの両方で -g オプションを指定し、デバッグ情報を出力します。
結果の値 3 は、11b なので、ビットが 1 になっているものが2個 (偶数個) あるため、PF が 1 になっています。
桁あふれもなく、値がゼロでもなく、符号は正、オーバーフローもないので、他のフラグは 0 です。
符号あり: 0xFF(-1) + 1 = 0
符号なしとして見た場合、255 + 1 の結果は 256 (0x0100) ですが、演算後に溢れた上位ビットは切り捨てられるため、結果の下位 8bit は、0 になります。
符号ありとして見た場合、0xFF は -1 です。-1 + 1 = 0 になるので、結果の値はどちらも変わりません。
符号なしとして見た場合は、結果が桁あふれしているので、CF が 1 になります。
結果の値 0 は、ビットが 1 になっているものはありませんが、偶数個としてみなされるので、PF は 1 になります。
結果の値が 0 になっているので、ZF は 1 になります。
AF については、とりあえず気にしないでください。
符号あり: -100 + 5 = -95 (0xA1)
符号なし/符号付きどちらの場合でも、結果の値は 0xA1 (1010_0001b) になります。
結果の最上位ビットが 1 になっているので、符号付きとして扱った場合は、負の値となり、SF (符号フラグ) が 1 になります。
ビットが 1 になっているものが3個 (奇数個) あるので、PF は 0 です。
符号あり: -100 + 120 = 20
どちらも、結果として 20 の値になります。
符号なしの場合は、桁あふれしているので、CF が 1 になります。
符号あり: 127 + 127 = 254 = -2 (0xFE)
符号なしの場合は、0〜255 までの値が表現できるので、254 になります。
符号ありの場合は、-128〜127 までしか表現できないので、結果の 254 (0xFE) は、符号付き 8bit では表現できず、負の値である -2 となります。
結果の最上位ビットが 1 になっているので、SF が 1 になります。
符号ありとして見た場合、正+正の結果が、本来ありえない負の値になっているので、OF が 1 になります。
これは、結果の値が、符号付きで表現できる値の範囲を超えたことを意味します。
ADD 命令で値を加算した場合、どのようなフラグがセットされるかを、実際に実行して確認してみます。
<test2.c>
void testfunc(void); int main(void) { testfunc(); return 0; }
<16_add.asm>
global testfunc section .text testfunc: mov al, 1 add al, 2 ; PF mov al, 0xff add al, 1 ; CF PF AF ZF mov al, -100 add al, 5 ; AF SF mov al, -100 add al, 120 ; CF PF AF mov al, 127 add al, 127 ; AF SF OF ret
C ソースの方は、testfunc 関数を呼び出すだけで、何もしません。
アセンブラコードでは、AL レジスタを使って、8bit 整数の加算を行います。
$ nasm -f elf64 -g 16_add.asm $ cc -g -o test test2.c 16_add.o
※フラグレジスタの値は、GDB のデバッガで確認するため、NASM と C コンパイラの両方で -g オプションを指定し、デバッグ情報を出力します。
デバッグ
出力された実行ファイルの test を gdb でデバッグして、ステータスフラグの値を確認してみます。
16_add.asm の testfunc 関数の先頭をブレークポイントに設定した後、r で実行します。
n でステップ実行していき、ADD 命令の実行が終わった後で、「p $eflags」でフラグレジスタの値を表示します。
(レジスタを参照する場合は、先頭に '$' を付けます)
なお、IF は割り込みを有効にするかどうかのフラグですが、ここでは気にしないでください。
それぞれの ADD 命令の後にフラグレジスタを表示すると、結果は以下のようになります。
$ gdb ./test (gdb) b 16_add.asm:testfunc (gdb) r 6 mov al, 1 (gdb) n 7 add al, 2 ; PF (gdb) n 9 mov al, 0xff (gdb) p $eflags $1 = [ PF IF ] ... # 終了 (gdb) q
16_add.asm の testfunc 関数の先頭をブレークポイントに設定した後、r で実行します。
n でステップ実行していき、ADD 命令の実行が終わった後で、「p $eflags」でフラグレジスタの値を表示します。
(レジスタを参照する場合は、先頭に '$' を付けます)
なお、IF は割り込みを有効にするかどうかのフラグですが、ここでは気にしないでください。
それぞれの ADD 命令の後にフラグレジスタを表示すると、結果は以下のようになります。
1 + 2 = 3 | PF |
---|---|
0xff + 1 = 0 | CF PF AF ZF |
-100 + 5 = -95 | AF SF |
-100 + 120 = 20 | CF PF AF |
127 + 127 = 254 | AF SF OF |
演算で変更されるフラグ
CF | キャリーフラグ。 加算時は、演算後の値が、符号なしの整数の最大値を超えた場合、1。 減算時は、A - B を符号なしで見た時に、A < B の場合 (結果が 0 より小さくなる場合)、1。 |
---|---|
PF | パリティフラグ。 結果の最下位 8bit 内に、ビットが 1 になっているものが偶数個ある場合 (0 を含む)、1。奇数個なら 0。 |
AF | 補助キャリーフラグ。 BCD 演算 (10進数演算) などで使いますが、BCD 演算は 64bit モードでは使えないので、基本的に使用する機会はありません。 |
ZF | ゼロフラグ。 結果の値が 0 なら、1。 |
SF | 符号フラグ。 結果の値が、符号ありで見た場合に、負の値 (最上位ビットが 1) になる場合、1。 |
OF | オーバーフローフラグ。 符号付き整数演算の結果、符号付きとして表現できる範囲の値を超えたことで、結果として正しくない形に符号が変化した場合、1。 加算時は、正+正=負 または 負+負=正 の場合。 減算時は、負−正=正 または 正−負=負 の場合。 |
それぞれの結果
1 + 2 = 3 [PF]
この結果は、特に問題はありません。結果の値 3 は、11b なので、ビットが 1 になっているものが2個 (偶数個) あるため、PF が 1 になっています。
桁あふれもなく、値がゼロでもなく、符号は正、オーバーフローもないので、他のフラグは 0 です。
0xff + 1 = 0 [CF PF AF ZF]
符号なし: 0xFF(255) + 1 = 256 (& 255) = 0符号あり: 0xFF(-1) + 1 = 0
符号なしとして見た場合、255 + 1 の結果は 256 (0x0100) ですが、演算後に溢れた上位ビットは切り捨てられるため、結果の下位 8bit は、0 になります。
符号ありとして見た場合、0xFF は -1 です。-1 + 1 = 0 になるので、結果の値はどちらも変わりません。
符号なしとして見た場合は、結果が桁あふれしているので、CF が 1 になります。
結果の値 0 は、ビットが 1 になっているものはありませんが、偶数個としてみなされるので、PF は 1 になります。
結果の値が 0 になっているので、ZF は 1 になります。
AF については、とりあえず気にしないでください。
-100 + 5 = -95 [AF SF]
符号なし: -100(0x9C=156) + 5 = 161 (0xA1)符号あり: -100 + 5 = -95 (0xA1)
符号なし/符号付きどちらの場合でも、結果の値は 0xA1 (1010_0001b) になります。
結果の最上位ビットが 1 になっているので、符号付きとして扱った場合は、負の値となり、SF (符号フラグ) が 1 になります。
ビットが 1 になっているものが3個 (奇数個) あるので、PF は 0 です。
-100 + 120 = 20 [CF PF AF]
符号なし: -100(0x9C=156) + 120 = 276 (& 255) = 20符号あり: -100 + 120 = 20
どちらも、結果として 20 の値になります。
符号なしの場合は、桁あふれしているので、CF が 1 になります。
127 + 127 = 254 [AF SF OF]
符号なし: 127 + 127 = 254 (0xFE)符号あり: 127 + 127 = 254 = -2 (0xFE)
符号なしの場合は、0〜255 までの値が表現できるので、254 になります。
符号ありの場合は、-128〜127 までしか表現できないので、結果の 254 (0xFE) は、符号付き 8bit では表現できず、負の値である -2 となります。
結果の最上位ビットが 1 になっているので、SF が 1 になります。
符号ありとして見た場合、正+正の結果が、本来ありえない負の値になっているので、OF が 1 になります。
これは、結果の値が、符号付きで表現できる値の範囲を超えたことを意味します。
オーバーフロー
オーバーフローフラグ (OF) は、値を符号付きとして扱った場合に、結果の値が、本来ありえない符号になった時に、1 になります。
加算時の A + B = C の符号の組み合わせは、以下のようになります。
上の3つは、式として正しい状態になりますが、下の2つは正しくありません。
正+正 は必ず正になるはずなのに、負の値になっています。負+負=正 の場合も同様です。
演算の結果が、本来ありえない符号になったということは、そのサイズの符号付き整数では、結果の値を表現することができないため、正しくない形で符号が反転したということです。
上記の正しくない2つの式を、加算ではなく減算で表現する場合は、2つ目のソース値の符号を反転して減算にすればいいので、「正+正=負」は、「正−負=負」と同等になります (A - -B = C → A + B = C)。
「負+負=正」も同様に、「負−正=正」で表現できます。
加算時の A + B = C の符号の組み合わせは、以下のようになります。
o 正+正=正 o 負+負=負 o 正+負 (負+正) =正 or 負 x 正+正=負 x 負+負=正
上の3つは、式として正しい状態になりますが、下の2つは正しくありません。
正+正 は必ず正になるはずなのに、負の値になっています。負+負=正 の場合も同様です。
演算の結果が、本来ありえない符号になったということは、そのサイズの符号付き整数では、結果の値を表現することができないため、正しくない形で符号が反転したということです。
上記の正しくない2つの式を、加算ではなく減算で表現する場合は、2つ目のソース値の符号を反転して減算にすればいいので、「正+正=負」は、「正−負=負」と同等になります (A - -B = C → A + B = C)。
「負+負=正」も同様に、「負−正=正」で表現できます。
減算
減算の SUB 命令は、第1オペランドから第2オペランドの値を引くことを除いて、ADD 命令とほぼ同じです。
第2オペランドの値を符号反転して ADD 命令を使ったとしても、結果の値は同じになります。
ただし、この場合、演算の結果として設定されるフラグが異なります。
SUB の場合、キャリーフラグが 1 になるのは、符号なしとして見た時に、operand1 < operand2 の場合です。
ADD の場合、0 + 0xFF = 255 なので、桁あふれは起こりません。そのため、CF = 0 となります。
第2オペランドの値を符号反転して ADD 命令を使ったとしても、結果の値は同じになります。
ただし、この場合、演算の結果として設定されるフラグが異なります。
; 0 - 1 と 0 + -1 の違い mov al, 0 sub al, 1 ; 0 - 1 = -1 (0xff) ; [SUB] CF PF AF SF mov al, 0 add al, -1 ; 0 + -1 = -1 (0xff) ; [ADD] PF SF
SUB の場合、キャリーフラグが 1 になるのは、符号なしとして見た時に、operand1 < operand2 の場合です。
ADD の場合、0 + 0xFF = 255 なので、桁あふれは起こりません。そのため、CF = 0 となります。
インクリメント/デクリメント
INC (インクリメント) は、値 1 を足すため、ADD dst, 1 と同じです。
DEC (デクリメント) は、値 1 を引くため、SUB dst, 1 と同じです。
ただし、ADD/SUB 命令と異なる点が1つあり、それは、CF フラグが変更されないことです。
INC 命令で、符号なしの最大値に 1 を足して、結果が 0 になったとしても、CF フラグは変更されません。
CF フラグを変更したい場合は、ADD/SUB 命令を使います。
DEC (デクリメント) は、値 1 を引くため、SUB dst, 1 と同じです。
ただし、ADD/SUB 命令と異なる点が1つあり、それは、CF フラグが変更されないことです。
INC 命令で、符号なしの最大値に 1 を足して、結果が 0 になったとしても、CF フラグは変更されません。
CF フラグを変更したい場合は、ADD/SUB 命令を使います。
キャリー付き加算/減算
64bit プロセッサであれば、64bit レジスタを使って、64bit 整数の演算を行うことができます。
しかし、場合によっては、それ以上 (128bit など) のサイズで演算を行いたいことがあります。
キャリーフラグを使うことで、繰り上げや繰り下げを考慮した加算/減算を行うことができるため、複数の値で構成された、大きな整数の演算を実装することが可能になります。
キャリー付き加算は ADC 命令で、キャリー付き減算は SBB 命令で実行することができます。
しかし、場合によっては、それ以上 (128bit など) のサイズで演算を行いたいことがあります。
キャリーフラグを使うことで、繰り上げや繰り下げを考慮した加算/減算を行うことができるため、複数の値で構成された、大きな整数の演算を実装することが可能になります。
キャリー付き加算は ADC 命令で、キャリー付き減算は SBB 命令で実行することができます。
サンプル
サンプルでは、値がわかりやすくなるように、BX:AX (16bit:16bit) の2つのレジスタを使って 32bit の値を格納し、16bit レジスタのみを使って、32bit の値を加算/減算することにします。
32bit 数値の 0x0001_FFFF + 0x0002_FFFF の結果を、BX:AX の2つのレジスタに格納するものとします。
32bit で計算すれば、結果は 0x0004_FFFE になりますが、これを、ADC 命令を使ったキャリー付き加算で実行してみます。
まず、0x0001_FFFF の値を BX:AX に格納します (EBX = 0x0001, EAX = 0xFFFF)。
次に、0x0002_FFFF の値を加算するため、下位の方の値から順に、16bit 値を加算していきます。
一番最初の加算時 (一番下位の値の加算時) は、通常の ADD 命令を使います (キャリーフラグは加算しないため)。
下位 16bit に加算する値は、0x0002_FFFF の下位 16bit なので、0xFFFF です。
AX レジスタに 0xFFFF を加算します。
0xFFFF + 0xFFFF の結果は 0x1FFFE ですが、上位ビットは切り捨てられるので、AX = 0xFFFE となります。
桁あふれが生じているので、CF は 1 になります。これはつまり、加算の繰り上げが発生しているということです。
次に、上位 16bit の加算を行います。
二回目以降の上位ビットの加算時は、ADC 命令を使います。ADC 命令は、現在の CF フラグ (0 or 1) の値も同時に加算します。
ADC 命令で、BX レジスタに、0x0002_FFFF の上位 16bit の 2 と、CF フラグの値を加算します。
前回の ADD 命令で、下位 16bit の繰り上げが発生しているため、CF は 1 になっています。
そのため、結果は、1 + 2 + CF (1) で、4 になります。
この結果、BX:AX には、0x0004:FFFE が格納され、16bit レジスタだけで、32bit の正しい値が演算できました。
より大きなサイズにしたいのであれば、数を増やすこともできます。
最下位の加算では ADD 命令を使い、それ以降は ADC 命令を使って加算していきます。
32bit 数値の 0x0001_FFFF + 0x0002_FFFF の結果を、BX:AX の2つのレジスタに格納するものとします。
32bit で計算すれば、結果は 0x0004_FFFE になりますが、これを、ADC 命令を使ったキャリー付き加算で実行してみます。
mov bx, 1 ; BX = 上位16bit mov ax, 0xffff ; AX = 下位16bit ; + 0x0002:ffff add ax, 0xffff ; 下位16bit加算: AX = 0xffff + 0xffff = 0xfffe, CF = 1 adc bx, 2 ; 上位16bit加算: BX = 1 + 2 + CF = 4
まず、0x0001_FFFF の値を BX:AX に格納します (EBX = 0x0001, EAX = 0xFFFF)。
次に、0x0002_FFFF の値を加算するため、下位の方の値から順に、16bit 値を加算していきます。
一番最初の加算時 (一番下位の値の加算時) は、通常の ADD 命令を使います (キャリーフラグは加算しないため)。
下位 16bit に加算する値は、0x0002_FFFF の下位 16bit なので、0xFFFF です。
AX レジスタに 0xFFFF を加算します。
0xFFFF + 0xFFFF の結果は 0x1FFFE ですが、上位ビットは切り捨てられるので、AX = 0xFFFE となります。
桁あふれが生じているので、CF は 1 になります。これはつまり、加算の繰り上げが発生しているということです。
次に、上位 16bit の加算を行います。
二回目以降の上位ビットの加算時は、ADC 命令を使います。ADC 命令は、現在の CF フラグ (0 or 1) の値も同時に加算します。
ADC 命令で、BX レジスタに、0x0002_FFFF の上位 16bit の 2 と、CF フラグの値を加算します。
前回の ADD 命令で、下位 16bit の繰り上げが発生しているため、CF は 1 になっています。
そのため、結果は、1 + 2 + CF (1) で、4 になります。
この結果、BX:AX には、0x0004:FFFE が格納され、16bit レジスタだけで、32bit の正しい値が演算できました。
より大きなサイズにしたいのであれば、数を増やすこともできます。
最下位の加算では ADD 命令を使い、それ以降は ADC 命令を使って加算していきます。
減算
キャリー付き減算についても同じです。
0x0001_00FF (65791) から 0x0000_0100 (256) を引いた場合、結果は 0x0000_FFFF (65535) となります。
下位 16bit の減算は、0x00FF(255) - 0x0100(256) = -1 (0xFFFF) です。
減算において、A < B なので、CF = 1 になります。つまり、繰り下げが起こります。
上位 16bit は、1 - 0 - CF (1) となるので、結果は 0 となります。
mov bx, 1 ; BX = 上位16bit mov ax, 0xff ; AX = 下位16bit ; 0x0001:00ff - 0x0000:0100 sub ax, 0x0100 ; 下位16bit: AX = 0x00ff - 0x0100 = 0xffff, CF = 1 sbb bx, 0 ; 上位16bit: BX = 1 - 0 - CF = 0
0x0001_00FF (65791) から 0x0000_0100 (256) を引いた場合、結果は 0x0000_FFFF (65535) となります。
下位 16bit の減算は、0x00FF(255) - 0x0100(256) = -1 (0xFFFF) です。
減算において、A < B なので、CF = 1 になります。つまり、繰り下げが起こります。
上位 16bit は、1 - 0 - CF (1) となるので、結果は 0 となります。