レジスタ
NASM の使い方はほぼ紹介し終えたので、ここからは、x64 の説明に移ります。
レジスタは、演算などを行う時に値を格納したり、メモリのアドレス位置を保持したりするために使われる、プロセッサ内部にある記憶回路です。
レジスタは、あらかじめ個数や名前が決まっています。
プログラマが自由に使用してもよい、一般的なレジスタは、
「汎用レジスタ (GPR = General-Purpose Register)」と呼ばれます。
他に、特殊な用途で使われる、「セグメントレジスタ」「フラグレジスタ」「命令ポインタ」などがあります。
※実際のレジスタ名は、使用するサイズによって異なります。
小文字の r が付いた表記は、レジスタの各サイズの名前を、総称して呼ぶ時に使います。
64bit モードの場合は、すべて 64bit サイズのレジスタとなります。
汎用レジスタは、命令のオペランドや、メモリのアドレス位置を指定する時に使用でき、プログラマが自由に値を変更して、使用することができます。
実際にレジスタを使う際は、以下のいずれかの名前を使います。
レジスタは、演算などを行う時に値を格納したり、メモリのアドレス位置を保持したりするために使われる、プロセッサ内部にある記憶回路です。
レジスタは、あらかじめ個数や名前が決まっています。
プログラマが自由に使用してもよい、一般的なレジスタは、
「汎用レジスタ (GPR = General-Purpose Register)」と呼ばれます。
他に、特殊な用途で使われる、「セグメントレジスタ」「フラグレジスタ」「命令ポインタ」などがあります。
汎用レジスタ
rAX | 主に演算などで使われます。 関数の戻り値としても使われます。 |
---|---|
rBX | 主に演算などで使われます。 |
rCX | 主に繰り返しのカウンタとして使われます。 |
rDX | 演算のソース値や格納先の一部として使われる場合があります。 |
rSI | メモリ転送のソースポインタとして使われます。 |
rDI | メモリ転送の宛先ポインタとして使われます。 |
rBP | スタックベースポインタとして使われます。 関数の引数や、ローカル変数を参照する時のベース位置です。 |
rSP | スタックポインタ (スタックの現在位置) として使われます。 |
r8〜r15 | x64 で追加された、8個のレジスタ |
※実際のレジスタ名は、使用するサイズによって異なります。
小文字の r が付いた表記は、レジスタの各サイズの名前を、総称して呼ぶ時に使います。
64bit モードの場合は、すべて 64bit サイズのレジスタとなります。
汎用レジスタは、命令のオペランドや、メモリのアドレス位置を指定する時に使用でき、プログラマが自由に値を変更して、使用することができます。
各サイズごとの名前
レジスタは、8/16/32/64bit の各サイズで扱うことができ、それぞれのサイズによって、レジスタを指定する時の名前が異なります。実際にレジスタを使う際は、以下のいずれかの名前を使います。
64bit | RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8〜R15 |
---|---|
下位32bit | EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, R8D〜R15D |
下位16bit | AX, BX, CX, DX, SI, DI, BP, SP, R8W〜R15W |
下位8bit | AL, BL, CL, DL, SIL, DIL, BPL, SPL, R8B〜R15B |
(16bit 中の) 上位8bit | AH, 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 レジスタとして使用されます。
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 として扱われます。
セグメントレジスタは、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) による指定を行うことができます。
RIP には、次に実行される命令のアドレス位置が格納されています。
命令のオペランドとして、直接指定することはできません (読み込みも変更も不可)。
64bit モードでは、メモリのアドレス指定をする際に、RIP 相対アドレス (RIP + offset) による指定を行うことができます。
フラグレジスタ (RFLAGS)
フラグレジスタには、2つの種類のフラグが設定されています。
RFLAGS は、リセット時に 02h に初期化されるため、アプリケーションから読み取れる値としては、0 となります。
- 演算や比較命令などを実行した時に設定され、アプリケーションからアクセスできるステータスフラグ。
- システムソフトウェアのみがアクセスできる、動作に関するシステムフラグ。
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 サイズ以外の場合は、他の範囲の値は変更されません。
EAX (32bit) に値がセットされた場合、RAX の上位 32bit は 0 になります。
AX/AL/AH に値がセットされた場合、上位 32bit には、元の値が残っています。
これにより、便利になることがいくつかあります。
例えば、符号なしの 32bit 値に値を加算して、64bit の結果を取得したい場合、通常であれば、32bit 値をゼロ拡張して、最初の値を 64bit レジスタに格納する必要がありますが、32bit レジスタに 32bit 値を格納するだけでゼロ拡張になるので、一つ手間が省けます。
逆に言うと、32bit から 64bit にゼロ拡張する命令は存在しないので、この方法を使うしかありません。
これは符号なしの演算を行いたい場合の方法ですが、符号付きで 64bit に拡張したい場合は、上位 32bit を 0xFFFF_FFFF にしなければならないので、MOVSXD 命令や CDQE 命令を使う必要があります。
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 命令を使って、レジスタからレジスタに同じサイズの値をコピーしたい場合、コンパイル後に出力されるバイナリは、以下のようになります。
先頭の 88 と 89 は、命令を示す 1byte のオペコードです。
/r は、ModRM (1 byte) の reg と r/m フィールドで、2つのオペランドを指定します。
8bit の転送なら 0x88、16/32/64bit の転送なら、0x89 のオペコードになります。
ここで、16/32/64bit の転送が、いずれも同じオペコードになっていますが、実際には、一連のバイトの前に、プレフィックスの 1 byte を付けることで、オペランドサイズを指定する必要があります。
SIL, DIL, BPL, SPL, r8〜r15 は、x64 モード時のみアクセスできるレジスタのため、これらをオペランドで指定したい場合は、REX プレフィックが必要になります。
なお、NASM は、これらのプレフィックスを自動で出力するので、アセンブラを書く時に意識する必要はありません。
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〜0x4F | REX プレフィックス。 上位 4bit は 4 で固定。下位 4bit はフラグになっており、可変。 64bit モード時のみ使用でき、x64 で拡張されたレジスタにアクセスする時などに必要となります。 |
SIL, DIL, BPL, SPL, r8〜r15 は、x64 モード時のみアクセスできるレジスタのため、これらをオペランドで指定したい場合は、REX プレフィックが必要になります。
なお、NASM は、これらのプレフィックスを自動で出力するので、アセンブラを書く時に意識する必要はありません。
デフォルトのオペランドサイズ
64bit モード時は、デフォルトのオペランドサイズは 32bit になります。
(プレフィックスを付けない場合、レジスタやメモリは 32bit サイズとして扱われる)
ただし、いくつかの命令では、デフォルトで (プレフィックスなしで) 64bit のオペランドサイズになる場合があります。
(プレフィックスを付けない場合、レジスタやメモリは 32bit サイズとして扱われる)
ただし、いくつかの命令では、デフォルトで (プレフィックスなしで) 64bit のオペランドサイズになる場合があります。