指令、寻址与控制流¶
x86-64 指令数量很多,但编译器输出主要由少数模式反复组合:传送数据、计算地址、运算、比较、跳转、调用和返回。阅读时应关注数据流和状态变化,而不是孤立背诵助记符。
Intel 语法基础¶
多数双操作数指令写作:
例如: 常见操作数类型: - 寄存器:rax
- 立即数:42、0x2a
- 内存:[rbp-8]
- 隐式操作数:某些乘除法、字符串和栈指令固定使用特定寄存器
x86 通常不允许普通 mov 同时以两个内存位置作为操作数,需要先经过寄存器。
mov 与扩展¶
mov 复制指定宽度的位:
mov 不设置算术标志位。读取内存的宽度由目标寄存器或显式大小决定。
lea:计算有效地址¶
lea 使用内存寻址表达式做算术,但不访问内存:
lea rax, [rbx] 解释为读取 [rbx],结果只是 rax = rbx。
x86-64 寻址形式¶
一般有效地址形式为:
-base:基址寄存器
- index:索引寄存器
- scale:1、2、4 或 8
- displacement:常量偏移
若 rbx=0x1000、rcx=3,有效地址是 0x1000 + 3*4 + 8 = 0x1014,随后读取 4 字节到 eax。
方括号表达式生成地址,实际读写宽度仍由指令决定。
算术指令¶
常见整数算术:
add/sub 会更新主要状态标志。inc/dec 不修改 CF,因此与加减 1 并非在所有上下文完全等价。
neg eax 计算 0 - eax。操作数非零时通常设置 CF。
乘除法¶
常见双操作数 imul:
mul/imul 使用隐式累加器,生成 rdx:rax 双倍宽度结果。div 是无符号除法,idiv 是有符号除法,64 位形式以 rdx:rax 作为被除数:
有符号除法前常用 cqo 把 rax 符号扩展到 rdx:rax。除数为零或商超出目标宽度会触发异常。
位运算与移位¶
-shl 左移,低位补 0。
- shr 逻辑右移,高位补 0。
- sar 算术右移,复制符号位。
- 可变移位计数通常放在 cl。
移位计数和 C 中负数除法的舍入语义需要谨慎对应,编译器可能增加修正指令。
标志寄存器¶
算术、逻辑和比较指令会修改 rflags 中部分位:
| 标志 | 含义 |
|---|---|
| ZF | 结果是否为 0 |
| SF | 结果最高位,即符号位 |
| CF | 无符号进位或借位 |
| OF | 有符号溢出 |
| PF | 低字节中 1 的数量是否为偶数 |
标志通常被紧随其后的条件跳转、setcc 或 cmovcc 消费。中间若出现会改标志的指令,前一次比较结果就可能失效。
cmp 与 test¶
cmp a, b 计算 a-b 并只更新标志,不保存结果:
test a, b 计算按位与并只更新标志:
test reg, reg 常用于检查零值,不会改变寄存器。
条件跳转¶
相等判断主要看 ZF:
| 指令 | 条件 |
|---|---|
| je / jz | ZF=1 |
| jne / jnz | ZF=0 |
无符号比较主要使用 CF 和 ZF:
| 指令 | 含义(在 cmp a,b 后) |
|---|---|
| ja | a > b |
| jae | a >= b |
| jb | a < b |
| jbe | a <= b |
有符号比较结合 SF 与 OF:
| 指令 | 含义(在 cmp a,b 后) |
|---|---|
| jg | a > b |
| jge | a >= b |
| jl | a < b |
| jle | a <= b |
同一位模式在有符号和无符号解释下排序可能不同,因此必须从变量来源和扩展方式判断语义。
无条件跳转与间接跳转¶
间接跳转常见于函数指针、虚函数、尾调用和switch 跳转表,也是控制流分析的难点。目标必须结合数据流和运行状态确定。
setcc 与 cmovcc¶
setcc 按条件把一个字节写为 0 或 1:
cmovcc 条件成立时复制值,避免短分支:
循环对照¶
可能抽象为:xor ecx, ecx ; i = 0
xor eax, eax ; sum = 0
.loop:
cmp ecx, esi
jge .done
add eax, [rdi+rcx*4]
inc ecx
jmp .loop
.done:
switch 与跳转表¶
连续 case 较多时,编译器可能: 1. 把输入减去最小 case 值。 2. 做无符号范围检查。 3. 用输入索引跳转表。 4. 间接跳转到对应代码块。 伪反汇编示意:
sub edi, 3
cmp edi, 5
ja .default
lea rax, [rip+.table]
movsxd rdx, dword ptr [rax+rdi*4]
add rax, rdx
jmp rax
call、ret 与尾调用¶
若函数完成自身工作后直接jmp other_function,可能是尾调用优化。此时不会创建新的返回地址,调用栈在调试器中可能不同于源码直觉。
nop 常用于对齐,int3 用于软件断点,lock 可让部分读改写指令具有原子性。Linux syscall 的参数为 rdi, rsi, rdx, r10, r8, r9,与普通 C 函数 ABI 不同。
阅读复杂函数时,应按跳转目标与分支结尾切分基本块,再把它们组成控制流图,而不是只做逐行翻译。
分析检查表¶
分析指令序列时依次确认:
1. 每条指令读什么、写什么、宽度多大?
2. 是否访问内存,最终地址如何计算?
3. 哪条指令最后设置了当前条件使用的标志?
4. 比较语义是有符号还是无符号?
5. 跳转是直接还是间接,目标如何得到?
6. 是否可能是编译器优化后的 cmov、尾调用或跳转表?
7. 是否能用等价 C 伪代码概括,而不过度猜测类型?
下一篇进入跨函数的数据流:函数与调用约定。