x86-64のレジスタと命令を読む方法
部分レジスタ、データ幅、MOVとLEAから機械語エンコーディングまで
x86-64のレジスタと命令を読む方法
本ブログの記事は講義を目的として作成しています。 実際のコードはCPU世代、OS、ABI、アセンブラ、コンパイラ、ビルド設定によって異なるため、対象環境を必ず確認してください。
rax、eax、ax、alは別々のレジスタに見えるが、実際には同じ保存領域の一部を異なる幅で参照する名前である。重要なのはニーモニックの丸暗記ではなく、どの幅を読み、どこへ書き、どの状態が変化するかを追跡することだ。
ソース、アセンブリ、機械語を分ける
C式 total += values[i];
アセンブリ表記 add rax, qword ptr [rcx + rdx*8]
機械語バイト 48 03 04 D1
Cは型と意図を表し、アセンブリは命令とオペランドを人間向けに表し、機械語はCPUがデコードするバイト列である。ソース1行と命令1個が一対一に対応する保証はない。
汎用レジスタと部分レジスタ
x86-64にはrax、rbx、rcx、rdx、rsi、rdi、rbp、rsp、r8〜r15の16本の64ビット汎用レジスタがある。rspはスタック命令が暗黙に使うが、どのレジスタに関数引数を置くかはABIが決める。
| 名前 | 参照範囲 |
|---|---|
rax | 64ビット全体 |
eax | 下位32ビット |
ax | 下位16ビット |
al | 下位8ビット |
ah | 8〜15ビットの旧来の名前 |
64ビットモードで32ビット汎用レジスタへ書くと、上位32ビットは0になる。
mov rax, 0FFFFFFFFFFFFFFFFh
mov eax, 7
; RAX = 0000000000000007h
8ビット・16ビット書き込みは残りのビットを保持する。
mov rax, 1122334455667788h
mov al, 0AAh
; RAX = 11223344556677AAh
したがってsetcc alの前にxor eax, eaxが現れることがある。ah、bh、ch、dhはREXプレフィックスが必要な命令では使用できないため、新しいコードでは避ける方が単純である。
データ幅は命令の意味の一部
mov al, byte ptr [rcx] ; 1バイト
mov ax, word ptr [rcx] ; 2バイト
mov eax, dword ptr [rcx] ; 4バイト
mov rax, qword ptr [rcx] ; 8バイト
同じアドレスでもアクセス幅が違う。幅を誤ると隣のフィールドまで読み書きし、ページ境界や構造体レイアウトの契約を破ることがある。
movと整数拡張
通常のmovはコピーであり、元を消さない。また一般的な形式ではメモリ同士を直接コピーできない。
mov rax, [rsi]
mov [rdi], rax
同じ0xFFでも符号なしなら255、符号付きなら-1である。
movzx eax, byte ptr [rcx] ; 255
movsx eax, byte ptr [rcx] ; -1を32ビットへ符号拡張
movsxd rax, eax ; signed 32ビットを64ビットへ
movzxは上位を0で、movsxは符号ビットで埋める。逆アセンブリにはCの型名が残らないため、拡張命令、比較、分岐を合わせてsignednessを推測する。
leaはメモリを読まない
lea rax, [rcx + rdx*4 + 16]
raxには実効アドレスが入る。そのアドレスのメモリ値は読まない。
lea rax, [rcx + 8] ; rax = rcx + 8
mov rax, [rcx + 8] ; その場所の8バイトを読む
lea eax, [rcx + rcx*2] ; eax = rcx * 3
コンパイラはポインタ計算だけでなく、フラグを変更しない加算や定数倍にもleaを使う。
算術とフラグ
Intel構文の2オペランド演算は通常、左側を上書きする。
add rax, rdx
sub rcx, 8
and eax, 0FFh
xor edx, edx ; rdx全体が0
これらは条件フラグも更新する。後続のjcc、setcc、cmovccがどの命令のフラグを読むか確認する。
Intel構文とAT&T構文
; Intel: 宛先が先
mov rax, qword ptr [rbx + 8]
# AT&T: 元が先
movq 8(%rbx), %rax
表記が違ってもCPU命令は同じだが、オペランド順序を取り違えるとデータフローが逆になる。
命令長は可変
x86-64命令は1〜15バイトで、prefix、opcode、ModR/M、SIB、displacement、immediateなどから構成される。
48 89 D8 mov rax, rbx
B8 2A 00 00 00 mov eax, 42
48 8B 44 D1 10 mov rax, [rcx + rdx*8 + 16]
バイト暗記は不要だが、命令境界、パッチ、開始位置が1バイトずれた逆アセンブルを調べる際に構造が役立つ。
小さな関数を追跡する
System V ABIでint transform(int x) { return x * 3 + 5; }は次のようになり得る。
transform:
lea eax, [rdi + rdi*2]
add eax, 5
ret
入力はedi、戻り値はeaxである。ただしハードウェアのwraparoundが、Cのsigned overflowまで定義するわけではない。
読み取りチェックリスト
- Intel構文かAT&T構文か。
- オペランド幅はいくつか。
- 何を読み、何を上書きするか。
- 32ビット書き込みで上位が0になるか。
- 実効アドレスはどう計算されるか。
- フラグを生成・消費するか。
- ABI上の引数・戻り値・保存の役割があるか。
まとめ
x86-64は状態変化として読む。部分レジスタは重なり、32ビット書き込みは上位を0にする。movはコピー、拡張命令は整数の解釈を示し、leaはメモリを読まず実効アドレスを計算する。