x86-64 汎用レジスタの逆引きガイド
x86系の解説記事では「AHに0x13をセットする」「ECXをループカウンタにする」のように、レジスタ名だけを与えられて使い方の指示だけが説明される場面がよくあります。この記事では、そもそもAHとALは何が違うのか、それぞれのレジスタが本来何のために存在し、現在のx86-64ではどう使われているのかを、逆引きできる形で整理します。
なぜ1つのレジスタに何種類も名前があるのか
x86-64の汎用レジスタは64ビット幅ですが、下位ビットだけを切り出して16ビット・8ビットのレジスタとして参照することもできます。これは新しい機能ではなく、16ビットCPUだった8086の時代から、上位互換を保ちながら拡張を重ねてきた結果です。
RAXを例にすると、ビット幅ごとの呼び方は次のようになります。
| 名前 | ビット幅 | 対応するビット範囲 |
|---|---|---|
RAX |
64bit | bit 0-63 |
EAX |
32bit | bit 0-31(RAXの下位32bit) |
AX |
16bit | bit 0-15(EAXの下位16bit) |
AH |
8bit | bit 8-15(AXの上位8bit) |
AL |
8bit | bit 0-7(AXの下位8bit) |
AHとALの違いは単純で、同じ16ビットレジスタAXを上位8bitと下位8bitに割って別名を付けているだけです。「AHに0x13をセットする」という指示は、AXという16ビットの入れ物のうち上位8bitだけを書き換える、という意味になります。
同じ命名規則がRBX/EBX/BX/BH/BL、RCX/ECX/CX/CH/CL、RDX/EDX/DX/DH/DLにもそのまま当てはまります。
一方、x86-64で新規に追加されたR8〜R15にはこの「上位8bit」に相当する名前がありません。R8であればR8D(32bit)・R8W(16bit)・R8B(8bit)という下位ビットの名前だけが存在し、R8Hのようなレジスタは存在しません。これは歴史的経緯の産物というより、後から追加されたレジスタに古い命名規則を無理に合わせなかった結果です。
RSI/RDI/RSP/RBPも同様の事情があります。8086由来の設計では、これらのレジスタには8bit単位でアクセスする手段がそもそもありませんでした(16bitのSI/DI/SP/BPまでしか存在しない)。x86-64になって初めてSIL/DIL/SPL/BPLという8bitアクセスが追加されています。
32bit書き込みは上位32bitをゼロクリアする
もう1つ紛らわしい挙動があります。EAXに書き込むとRAXの上位32bitは自動的に0でクリアされますが、AXやALに書き込んでもRAXの上位ビットはそのまま残ります。
mov eax, 0x1 ; RAX全体が 0x0000000000000001 になる(上位32bitがクリアされる)
mov ax, 0x1 ; 上位48bitはそれ以前の値のまま変化しない64bitレジスタ全体を明示的に使う命令(mov rax, ...など)以外で、意図せず上位ビットが変わったり残ったりする、というのはこの非対称な仕様によるものです。
AH/BH/CH/DHとR8〜R15は同じ命令の中で共存できない
R8〜R15のような拡張レジスタを使う命令には、CPUに追加情報を伝えるためのREXプレフィックスというバイトが付与されます。このREXプレフィックスが付いている命令では、AH/BH/CH/DHを指すはずだったビットパターンが、代わりにSPL/BPL/SIL/DILを指すよう再定義されています。そのため、同一命令内でAHとR8のような拡張レジスタを同時に使うことはできません。アセンブラがこの組み合わせをエラーにするのはこのためです。
汎用レジスタの伝統的な役割
現在のx86-64では、汎用レジスタはほとんどの演算命令でどれを使っても構わない「汎用」的な存在になっていますが、一部の命令は特定のレジスタを暗黙のオペランドとして使うため、名前に由来する伝統的な役割が今も残っています。
| レジスタ | 由来 | 現在も残る役割 |
|---|---|---|
RAX |
Accumulator(累算器) | 演算結果や関数の戻り値を格納する慣習。mul/div命令は暗黙にRAXを使う |
RBX |
Base(ベース) | 特別な暗黙の役割は薄いが、呼び出し規約では両ABI共通でcallee-saved(後述) |
RCX |
Counter(カウンタ) | loop命令やshl/shrなどシフト系命令の回数レジスタとして暗黙に使われる |
RDX |
Data(データ) | mul/div命令でRAXと組になり128bit/64bit値の上位ビットを持つ |
RSI |
Source Index | 文字列命令(movsなど)でのコピー元アドレス |
RDI |
Destination Index | 文字列命令でのコピー先アドレス |
RBP |
Base Pointer | 現在のスタックフレームの基準点として使う慣習(省略される最適化も一般的) |
RSP |
Stack Pointer | スタックの先頭を指す。push/pop/call/retが暗黙に読み書きする |
R8〜R15 |
(x86-64での追加分) | 伝統的な暗黙の役割は無いが、呼び出し規約で引数レジスタとして割り当てられる |
特殊なレジスタ:RIP・RFLAGS・セグメントレジスタ
汎用レジスタ以外にも、CPUの状態を保持する専用レジスタがあります。
RIP(Instruction Pointer):次に実行する命令のアドレスを保持します。movで直接書き換えることはできず、jmp・call・retのような制御フロー命令を通してのみ変更できます。セグメントレジスタCSの切り替えにファージャンプ/ファーリターンという特殊な手段が必要になるのも、RIPを直接書き換えるmovが存在しないことが理由の一つですRFLAGS:演算結果のフラグ(ZF:ゼロフラグ、CF:キャリーフラグなど)と、CPUの動作を制御するフラグ(IF:割り込み許可フラグなど)をまとめて保持するレジスタです。sti/cli命令は、このレジスタのIFビットだけを操作します- セグメントレジスタ(
CS/SS/DS/ES/FS/GS):ロングモードではCSとSS以外のbase/limitはCPUに無視されますが、CSは現在の実行コードセグメントと特権レベルの指標として、SSはスタック操作の対象として引き続き意味を持ちます。FS/GSはTLS(スレッドローカルストレージ)や、OSがCPUコアごとのデータ構造を指すためのベースアドレスとして、現代のOSカーネルでもよく使われます
呼び出し規約における引数レジスタの違い
Linux等ではSysV ABIが、Windows/UEFI環境ではMicrosoft x64 ABIが使われており、関数の引数をどのレジスタに渡すかが異なります。
| 引数の順番 | SysV x64(Linux等) | Microsoft x64(UEFI/Windows) |
|---|---|---|
| 第1引数 | RDI |
RCX |
| 第2引数 | RSI |
RDX |
| 第3引数 | RDX |
R8 |
| 第4引数 | RCX |
R9 |
| 第5引数以降 | R8, R9, ... |
スタック渡し |
| 戻り値 | RAX |
RAX |
どのレジスタを呼び出し先が保存すべきか(callee-saved)も規約によって異なります。SysVではRBX・RSP・RBP・R12〜R15が、Microsoft x64ではRBX・RBP・RDI・RSI・RSP・R12〜R15がcallee-savedです。呼び出し先の関数がこれらのレジスタの値を変更する場合、関数の中で退避・復帰する責任を負います。
制御レジスタ(CR0〜CR4)
汎用レジスタとは別に、CPU自体の動作モードを制御する「制御レジスタ」があります。汎用の演算には使えず、movの特殊な形式でのみアクセスできます。
CR0:保護モードの有効化(PEビット)やページングの有効化(PGビット)など、CPUの基本的な動作モードを制御しますCR2:ページフォルト(#PF)が発生した際、CPUがフォルトの原因となった仮想アドレスを自動的に格納するレジスタです。ページングを実装する際、ハンドラ側でこの値を読むことになりますCR3:ページテーブルの物理ベースアドレスを保持しますCR4:PAE(物理アドレス拡張)やSSE命令の有効化など、より新しい拡張機能の有効/無効を切り替えるビット群を持ちます
参考リンク
- OSDev Wiki: CPU Registers x86-64 —— レジスタごとのビット幅・別名の一覧です
- Intel 64 and IA-32 Architectures Software Developer's Manual —— レジスタとフラグの正確な定義です(Volume 1, Chapter 3)
- Microsoft: x64 calling convention —— Microsoft x64 ABIにおける引数レジスタとcallee-saved/caller-savedの一覧です
- System V Application Binary Interface AMD64 Architecture Processor Supplement —— SysV x64 ABIの一次情報源です