加算/減算

演算命令
演算命令の一覧は、汎用命令 (2) でご覧ください。

加算と減算は、基本的に符号付き/符号なしの数値を区別しません (結果としてはどちらも同じ値になるため)。
結果の値がどうなったかは、rFLAGS レジスタのステータスフラグで判断できます。
加算のサンプルコード
汎用の演算命令を実行すると、結果がどうなったかによって、rFLAGS レジスタのステータスフラグが、セットまたはクリアされます。

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 でデバッグして、ステータスフラグの値を確認してみます。

$ 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 = 3PF
0xff + 1 = 0CF PF AF ZF
-100 + 5 = -95AF SF
-100 + 120 = 20CF PF AF
127 + 127 = 254AF 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 の符号の組み合わせは、以下のようになります。

o 正+正=正
o 負+負=負
o 正+負 (負+正) =正 or 負
x 正+正=負
x 負+負=正

上の3つは、式として正しい状態になりますが、下の2つは正しくありません。
正+正 は必ず正になるはずなのに、負の値になっています。負+負=正 の場合も同様です。

演算の結果が、本来ありえない符号になったということは、そのサイズの符号付き整数では、結果の値を表現することができないため、正しくない形で符号が反転したということです。

上記の正しくない2つの式を、加算ではなく減算で表現する場合は、2つ目のソース値の符号を反転して減算にすればいいので、「正+正=負」は、「正−負=負」と同等になります (A - -B = C → A + B = C)。
「負+負=正」も同様に、「負−正=正」で表現できます。
減算
減算の SUB 命令は、第1オペランドから第2オペランドの値を引くことを除いて、ADD 命令とほぼ同じです。

第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 命令を使います。
キャリー付き加算/減算
64bit プロセッサであれば、64bit レジスタを使って、64bit 整数の演算を行うことができます。
しかし、場合によっては、それ以上 (128bit など) のサイズで演算を行いたいことがあります。

キャリーフラグを使うことで、繰り上げや繰り下げを考慮した加算/減算を行うことができるため、複数の値で構成された、大きな整数の演算を実装することが可能になります。

キャリー付き加算は ADC 命令で、キャリー付き減算は SBB 命令で実行することができます。
サンプル
サンプルでは、値がわかりやすくなるように、BX:AX (16bit:16bit) の2つのレジスタを使って 32bit の値を格納し、16bit レジスタのみを使って、32bit の値を加算/減算することにします。

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 命令を使って加算していきます。
減算
キャリー付き減算についても同じです。

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 となります。