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)

AHALの違いは単純で、同じ16ビットレジスタAXを上位8bitと下位8bitに割って別名を付けているだけです。「AHに0x13をセットする」という指示は、AXという16ビットの入れ物のうち上位8bitだけを書き換える、という意味になります。

同じ命名規則がRBX/EBX/BX/BH/BLRCX/ECX/CX/CH/CLRDX/EDX/DX/DH/DLにもそのまま当てはまります。

一方、x86-64で新規に追加されたR8R15にはこの「上位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でクリアされますが、AXALに書き込んでもRAXの上位ビットはそのまま残ります。

mov eax, 0x1  ; RAX全体が 0x0000000000000001 になる(上位32bitがクリアされる)
mov ax, 0x1   ; 上位48bitはそれ以前の値のまま変化しない

64bitレジスタ全体を明示的に使う命令(mov rax, ...など)以外で、意図せず上位ビットが変わったり残ったりする、というのはこの非対称な仕様によるものです。

AH/BH/CH/DHとR8〜R15は同じ命令の中で共存できない

R8R15のような拡張レジスタを使う命令には、CPUに追加情報を伝えるためのREXプレフィックスというバイトが付与されます。このREXプレフィックスが付いている命令では、AH/BH/CH/DHを指すはずだったビットパターンが、代わりにSPL/BPL/SIL/DILを指すよう再定義されています。そのため、同一命令内でAHR8のような拡張レジスタを同時に使うことはできません。アセンブラがこの組み合わせをエラーにするのはこのためです。

汎用レジスタの伝統的な役割

現在の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が暗黙に読み書きする
R8R15 (x86-64での追加分) 伝統的な暗黙の役割は無いが、呼び出し規約で引数レジスタとして割り当てられる

特殊なレジスタ:RIP・RFLAGS・セグメントレジスタ

汎用レジスタ以外にも、CPUの状態を保持する専用レジスタがあります。

  • RIP(Instruction Pointer):次に実行する命令のアドレスを保持します。movで直接書き換えることはできず、jmpcallretのような制御フロー命令を通してのみ変更できます。セグメントレジスタCSの切り替えにファージャンプ/ファーリターンという特殊な手段が必要になるのも、RIPを直接書き換えるmovが存在しないことが理由の一つです
  • RFLAGS:演算結果のフラグ(ZF:ゼロフラグ、CF:キャリーフラグなど)と、CPUの動作を制御するフラグ(IF:割り込み許可フラグなど)をまとめて保持するレジスタです。sti/cli命令は、このレジスタのIFビットだけを操作します
  • セグメントレジスタ(CS/SS/DS/ES/FS/GS:ロングモードではCSSS以外の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ではRBXRSPRBPR12R15が、Microsoft x64ではRBXRBPRDIRSIRSPR12R15がcallee-savedです。呼び出し先の関数がこれらのレジスタの値を変更する場合、関数の中で退避・復帰する責任を負います。

制御レジスタ(CR0〜CR4)

汎用レジスタとは別に、CPU自体の動作モードを制御する「制御レジスタ」があります。汎用の演算には使えず、movの特殊な形式でのみアクセスできます。

  • CR0:保護モードの有効化(PEビット)やページングの有効化(PGビット)など、CPUの基本的な動作モードを制御します
  • CR2:ページフォルト(#PF)が発生した際、CPUがフォルトの原因となった仮想アドレスを自動的に格納するレジスタです。ページングを実装する際、ハンドラ側でこの値を読むことになります
  • CR3:ページテーブルの物理ベースアドレスを保持します
  • CR4:PAE(物理アドレス拡張)やSSE命令の有効化など、より新しい拡張機能の有効/無効を切り替えるビット群を持ちます

参考リンク