如何阅读 x86-64 寄存器与指令
从部分寄存器、数据大小、MOV 与 LEA 到机器码编码
如何阅读 x86-64 寄存器与指令
本博客文章以教学为目的。 实际代码会随 CPU 世代、操作系统、ABI、汇编器、编译器与构建选项而变化,请先确认目标环境。
初次看到 rax、eax、ax、al 时,它们很像四个不同的寄存器。真正重要的不是背下所有助记符,而是追踪:读取多宽的值、值从哪里来、写到哪里,以及哪些处理器状态被改变。
分清三个层次
C 表达式 total += values[i];
汇编语言表示 add rax, qword ptr [rcx + rdx*8]
机器码字节 48 03 04 D1
C 表达类型与意图;汇编把指令和操作数写成人可读形式;机器码则是 CPU 解码的字节。源码一行与指令一条并没有一对一保证。
通用寄存器与部分寄存器
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
但这不是“x86 指令永远只能接触一个内存位置”的普遍规则;字符串和 read-modify-write 指令有自己的格式,必须查阅指令手册。
扩展方式显示 signedness
同一个字节 0xFF 可以是无符号 255,也可以是有符号 -1。
movzx eax, byte ptr [rcx] ; 255
movsx eax, byte ptr [rcx] ; 符号扩展为 -1
movsxd rax, eax ; signed 32 位扩展到 64 位
movzx 用 0 填充新高位,movsx 复制原符号位。反汇编中没有 C 类型名,因此要把扩展、比较与分支一起看,才能推测有符号或无符号语义。
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 执行不改变标志位的加法与常数倍运算。
算术指令覆盖目的操作数
add rax, rdx
sub rcx, 8
and eax, 0FFh
xor edx, edx ; rdx 整体也变成 0
Intel 语法的双操作数指令通常是 destination = destination op source。这些指令也更新条件标志;后面的 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]
不必背字节,但在检查指令边界、补丁、重定位,或反汇编起点偏移一个字节时,这个结构非常重要。
追踪一个小函数
System V ABI 下,int transform(int x) { return x * 3 + 5; } 可能成为:
transform:
lea eax, [rdi + rdi*2]
add eax, 5
ret
输入在 edi,返回值在 eax。但硬件环绕并不等于 C 的 signed overflow 语义;后者是未定义行为。
阅读检查表
- 是 Intel 还是 AT&T 语法?
- 操作数宽度是多少?
- 读取什么、覆盖什么?
- 32 位写入是否把高位清零?
- 有效地址如何计算?
- 是否产生或消费标志?
- ABI 是否赋予参数、返回或保存角色?
总结
阅读 x86-64 要从状态变化开始。部分寄存器重叠,32 位写入清除高半部;mov 复制数据,扩展指令暴露整数解释方式,而 lea 不读内存,只计算有效地址。