レジスタ

レジスタ
NASM の使い方はほぼ紹介し終えたので、ここからは、x64 の説明に移ります。

レジスタは、演算などを行う時に値を格納したり、メモリのアドレス位置を保持したりするために使われる、プロセッサ内部にある記憶回路です。
レジスタは、あらかじめ個数や名前が決まっています。

プログラマが自由に使用してもよい、一般的なレジスタは、
「汎用レジスタ (GPR = General-Purpose Register)」と呼ばれます。

他に、特殊な用途で使われる、「セグメントレジスタ」「フラグレジスタ」「命令ポインタ」などがあります。
汎用レジスタ
rAX主に演算などで使われます。
関数の戻り値としても使われます。
rBX主に演算などで使われます。
rCX主に繰り返しのカウンタとして使われます。
rDX演算のソース値や格納先の一部として使われる場合があります。
rSIメモリ転送のソースポインタとして使われます。
rDIメモリ転送の宛先ポインタとして使われます。
rBPスタックベースポインタとして使われます。
関数の引数や、ローカル変数を参照する時のベース位置です。
rSPスタックポインタ (スタックの現在位置) として使われます。
r8〜r15x64 で追加された、8個のレジスタ

※実際のレジスタ名は、使用するサイズによって異なります。
小文字の r が付いた表記は、レジスタの各サイズの名前を、総称して呼ぶ時に使います。

64bit モードの場合は、すべて 64bit サイズのレジスタとなります。

汎用レジスタは、命令のオペランドや、メモリのアドレス位置を指定する時に使用でき、プログラマが自由に値を変更して、使用することができます。

各サイズごとの名前
レジスタは、8/16/32/64bit の各サイズで扱うことができ、それぞれのサイズによって、レジスタを指定する時の名前が異なります。
実際にレジスタを使う際は、以下のいずれかの名前を使います。

64bitRAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8〜R15
下位32bitEAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, R8D〜R15D
下位16bitAX, BX, CX, DX, SI, DI, BP, SP, R8W〜R15W
下位8bitAL, BL, CL, DL, SIL, DIL, BPL, SPL, R8B〜R15B
(16bit 中の) 上位8bitAH, BH, CH, DH
MMX レジスタ
MMX 命令を使用する場合、MMX0〜MMX7 (64bit) のレジスタが使用できます。
XMM,YMM レジスタ
SSE 命令では、XMM0〜XMM15 (128bit) レジスタが使用できます。
SSE/AVX 命令では、XMM レジスタに加えて、YMM0〜YMM15 (256bit) レジスタが使用できます。

x86 では、SSE 命令で XMM0〜XMM7 のレジスタが使用できましたが、x64 では 16 個に拡張されました。

ただし、XMM と YMM レジスタは、個別に存在しているわけではありません。
YMM レジスタの下位 128bit が、XMM レジスタとして使用されます。
セグメントレジスタ
CS, DS, ES, SS, FS, GS

セグメントレジスタは、16bit レジスタです。
主に 16/32bit プログラムで使用するため、64bit モードでは基本的に使いません。

CS はコードセグメント、DS はデータセグメント、ES はエクストラセグメント、SS はスタックセグメントです。FS, GS は補助的に使います。
16/32bit では、複数のセグメントが存在するため、セグメントレジスタとオフセット値で、アドレス指定を行う形になります。

64bit モードでは、セグメントは一つであり、コードやデータはすべて連続した形でロードされるので、CS, DS, ES, SS は、常に 0 として扱われます。
フラグレジスタ
FLAGS (16bit), EFLAGS (32bit), RFLAGS (64bit)

フラグレジスタは、動作に関するフラグがセットされたレジスタです。
演算時などに、対応するビットが設定/クリアされます。
命令ポインタ
RIP (64bit)

RIP には、次に実行される命令のアドレス位置が格納されています。
命令のオペランドとして、直接指定することはできません (読み込みも変更も不可)。

64bit モードでは、メモリのアドレス指定をする際に、RIP 相対アドレス (RIP + offset) による指定を行うことができます。
フラグレジスタ (RFLAGS)
フラグレジスタには、2つの種類のフラグが設定されています。

  • 演算や比較命令などを実行した時に設定され、アプリケーションからアクセスできるステータスフラグ。
  • システムソフトウェアのみがアクセスできる、動作に関するシステムフラグ。

RFLAGS は、リセット時に 02h に初期化されるため、アプリケーションから読み取れる値としては、0 となります。
ステータスフラグ
通常のアプリケーションでは、以下のステータスフラグのみを操作します。

|15 - 12|11|10|9|8| 7| 6| 5| 4| 3| 2| 1| 0| bit
|-------|OF|DF|---|SF|ZF|--|AF|--|PF|--|CF|

CFキャリーフラグ (bit0)
演算の結果、桁あふれが発生した場合、1 に設定され、それ以外の場合は 0 になります。
PFパリティフラグ (bit2)
特定の演算において、結果の最下位バイトに、ビットが 1 になっているものが偶数個ある場合、1 に設定され、それ以外の場合 (奇数個の場合) は 0 になります。
AF補助キャリーフラグ (bit4)
算術演算、または BCD 演算により、bit3 から桁あふれ (加算の場合) または繰り下がり (減算の場合) が生成される場合、1 に設定され、それ以外の場合は 0 になります。
ZFゼロフラグ (bit6)
最後の算術演算の結果が 0 の場合、1。それ以外は 0 になります。
SF符号フラグ (bit7)
最後の算術演算の結果が負の値になった場合、1。それ以外は 0 になります。
符号フラグは、結果の値の、最上位ビットと同じになります。
DF方向フラグ (bit10)
MOVSx 命令などの、メモリ操作で使用される rSI, rDI レジスタの値を、加算するか減算するかのフラグ。
0 で加算、1 で減算となります。
STD/CLD 命令を使用して、フラグをセットまたはクリアできます。
OFオーバーフローフラグ (bit11)
最後の符号付き整数演算の結果が、意図しない符号になった場合は 1。それ以外は 0 になります。
1 の場合、結果の値が、符号付きで表現できる最大値より大きすぎる (オーバーフロー) か、最小値より小さすぎる (アンダーフロー) ことを意味します。
64bit モードのゼロ拡張
64bit モードで、命令の宛先オペランドに 32bit レジスタが指定され、そこに値が格納される場合、そのレジスタの上位 32bit は 0 にクリアされます (64bit ゼロ拡張)。

MOV 命令だけでなく、基本的に他のすべての命令でも、同じ動作になります (一部例外あり)。

ただし、宛先のレジスタが 32bit サイズ以外の場合は、他の範囲の値は変更されません。

; テスト用の値 (64bit)
mov rbx, 0x1111_2222_3333_4444

; 32bit -> 0000_0000_FFFF_FFFF (上位32bit は 0 になる)
mov rax, rbx
mov eax, 0xFFFF_FFFF

; 16bit -> 1111_2222_3333_0000
mov rax, rbx
mov ax, 0

; 下位8bit -> 1111_2222_3333_4400
mov rax, rbx
mov al, 0

; 上位8bit -> 1111_2222_3333_0044
mov rax, rbx
mov ah, 0

EAX (32bit) に値がセットされた場合、RAX の上位 32bit は 0 になります。
AX/AL/AH に値がセットされた場合、上位 32bit には、元の値が残っています。

これにより、便利になることがいくつかあります。

例えば、符号なしの 32bit 値に値を加算して、64bit の結果を取得したい場合、通常であれば、32bit 値をゼロ拡張して、最初の値を 64bit レジスタに格納する必要がありますが、32bit レジスタに 32bit 値を格納するだけでゼロ拡張になるので、一つ手間が省けます。

mov eax, 0xFFFF_FFFF ; 上位32bit は 0 になる
add rax, 1           ; 0x0000_0001_0000_0000

逆に言うと、32bit から 64bit にゼロ拡張する命令は存在しないので、この方法を使うしかありません。

これは符号なしの演算を行いたい場合の方法ですが、符号付きで 64bit に拡張したい場合は、上位 32bit を 0xFFFF_FFFF にしなければならないので、MOVSXD 命令や CDQE 命令を使う必要があります。
オペランドサイズ
例えば、MOV 命令を使って、レジスタからレジスタに同じサイズの値をコピーしたい場合、コンパイル後に出力されるバイナリは、以下のようになります。

MOV reg/mem8,  reg8  | 88 /r
MOV reg/mem16, reg16 | 89 /r
MOV reg/mem32, reg32 | 89 /r
MOV reg/mem64, reg64 | 89 /r

先頭の 88 と 89 は、命令を示す 1byte のオペコードです。
/r は、ModRM (1 byte) の reg と r/m フィールドで、2つのオペランドを指定します。

8bit の転送なら 0x88、16/32/64bit の転送なら、0x89 のオペコードになります。

ここで、16/32/64bit の転送が、いずれも同じオペコードになっていますが、実際には、一連のバイトの前に、プレフィックスの 1 byte を付けることで、オペランドサイズを指定する必要があります。

mov al, al    ; 88 C0 (8bit)
mov ah, ah    ; 88 E4 (8bit)

mov ax, ax    ; 66 89 C0 (16bit: 66)
mov eax, eax  ; 89 C0    (32bit: デフォルト)
mov rax, rax  ; 48 89 C0 (64bit: REX)
mov r8b, r8b  ; 45 88 C0 (8bit: REX)

66, 48, 45 はプレフィックスのバイトです。
プレフィックス
0x66オペランドサイズ・プレフィックス。

64bit モード、または互換/レガシーモードの 32bit 時に、このプレフィックスがある場合、16bit のオペランドサイズを指定することになります。
互換/レガシーモードの 16bit 時は、プレフィックスありで 32bit。プレフィックスなしで 16bit のオペランドサイズになります。
0x40〜0x4FREX プレフィックス。
上位 4bit は 4 で固定。下位 4bit はフラグになっており、可変。
64bit モード時のみ使用でき、x64 で拡張されたレジスタにアクセスする時などに必要となります。

SIL, DIL, BPL, SPL, r8〜r15 は、x64 モード時のみアクセスできるレジスタのため、これらをオペランドで指定したい場合は、REX プレフィックが必要になります。

なお、NASM は、これらのプレフィックスを自動で出力するので、アセンブラを書く時に意識する必要はありません。
デフォルトのオペランドサイズ
64bit モード時は、デフォルトのオペランドサイズは 32bit になります。
(プレフィックスを付けない場合、レジスタやメモリは 32bit サイズとして扱われる)

ただし、いくつかの命令では、デフォルトで (プレフィックスなしで) 64bit のオペランドサイズになる場合があります。