指令详解
MOV
用法 MOV destination, source。寄存器直接写寄存器的名称reg,内存写[addr],立即数直接写数字。
注意事项及规则 操作数大小需要匹配
寄存器
位宽 累加 基址 计数 数据 栈指针 基址指针 源变址 目的变址 扩展 64位 rax rbx rcx rdx rsp rbp rsi rdi r8 ~ r15 32位 eax ebx ecx edx esp ebp esi edi r8d ~ r15d 16位 ax bx cx dx sp bp si di r8w ~ r15w 低8位 al bl cl dl spl bpl sil dil r8b ~ r15b 高8位 ah bh ch dh — — — — — 规则
- 两个操作数的位宽必须一致:
mov eax, ebx合法,mov eax, bx不合法 - 不能内存到内存:
mov [addr1], [addr2]不合法,需借助寄存器中转 - 立即数不能作为目的操作数
- 段寄存器不能由立即数直接装载,需先装入通用寄存器再转
- 64位模式下写32位寄存器(如
mov eax, 1)会自动清零对应64位寄存器的高32位,写16位或8位寄存器则不会
- 两个操作数的位宽必须一致:
示例
1
2
3
4
5mov eax, 0x1234 ; 立即数 → 寄存器
mov rax, rbx ; 寄存器 → 寄存器
mov eax, [0x404000] ; 内存 → 寄存器
mov [rbp-0x10], rdx ; 寄存器 → 内存
mov byte [rdi], 0x41 ; 立即数 → 内存,需用 byte/word/dword/qword 指明位宽
LEA
用法 LEA destination, [address_expression]。计算方括号中地址表达式的结果(有效地址),直接存入目的寄存器,不会访问该地址处的内存。
- 与MOV的区别
lea rax, [rbx+8]得到的是 rbx+8 这个地址本身;而mov rax, [rbx+8]得到的是该地址处存储的值 - 常见用途
- 取变量或字符串的地址,如
lea rdi, [rip+0x2004](位置无关代码中取数据地址的常见写法) - 快速算术运算,一条指令完成乘加:
lea rax, [rbx+rcx*4+0x10]即 rax = rbx + rcx*4 + 0x10
- 取变量或字符串的地址,如
- 注意事项 LEA 不影响任何标志位;目的操作数只能是寄存器
CALL
用法 CALL target。先把返回地址(CALL 的下一条指令的地址)压入栈,再跳转到目标地址执行,相当于”push 返回地址 + jmp target”的组合。
与RET配对 被调用函数末尾的
ret指令从栈顶弹出返回地址并跳回去,相当于pop rip。如果栈不平衡(push/pop 不配对),ret 就会跳到错误的地址——栈溢出之所以能劫持控制流,利用的正是这一点调用形式
1
2
3call func ; 直接调用,目标地址编译期确定
call rax ; 间接调用,目标地址在寄存器中
call qword [rbx] ; 间接调用,目标地址在内存中(如 GOT 表、虚表)参数传递 x86-64 Linux 下前 6 个整型/指针参数依次走 rdi、rsi、rdx、rcx、r8、r9,其余压栈,返回值放 rax;32 位下参数全部从右往左压栈
JUMP
用法 JMP target 为无条件跳转;Jcc target 为条件跳转,根据标志寄存器决定是否跳转,通常跟在 CMP、TEST 之后。
无条件跳转
jmp func、jmp rax、jmp qword [rbx],形式与 CALL 相同,但不压返回地址条件跳转
指令 跳转条件 含义 JE / JZ ZF=1 相等 / 结果为零 JNE / JNZ ZF=0 不相等 / 结果非零 JA / JNBE CF=0 且 ZF=0 无符号大于 JAE / JNB CF=0 无符号大于等于 JB / JNAE CF=1 无符号小于 JBE / JNA CF=1 或 ZF=1 无符号小于等于 JG / JNLE ZF=0 且 SF=OF 有符号大于 JGE / JNL SF=OF 有符号大于等于 JL / JNGE SF≠OF 有符号小于 JLE / JNG ZF=1 或 SF≠OF 有符号小于等于 JS / JNS SF=1 / SF=0 结果为负 / 非负 JO / JNO OF=1 / OF=0 溢出 / 未溢出 JC / JNC CF=1 / CF=0 有进位 / 无进位 JP / JNP PF=1 / PF=0 低8位中1的个数为偶 / 为奇
TEST
用法 TEST destination, source。TEST进行的是与运算,只不过不保存计算结果的值,只根据结果设置标志寄存器SF、ZF、PF,并清除CF、OF。
标志寄存器
标志位 名称 Set时的含义 Cleared时的含义 CF 进位标志 进位 没进位 OF 溢出标志 溢出 未溢出 SF 符号标志 负数 正数 ZF 零标志 零 非零 PF 奇偶标志 低8位中1的个数是偶数 低8位中1的个数是奇数数
CMP
用法 CMP destination, source。CMP进行的是减运算,即destination-source,但不保存计算结果的值,只根据结果设置标志寄存器
无符号数的比较
看 CF 和 ZF:若 destination < source(不够减,产生借位)则 CF=1,两者相等则 ZF=1。对应跳转指令:大于 JA/JNBE、大于等于 JAE/JNB、小于 JB/JNAE、小于等于 JBE/JNA
有符号数的比较
看 SF、OF、ZF:相等则 ZF=1,destination < source 时 SF≠OF。对应跳转指令:大于 JG/JNLE、大于等于 JGE/JNL、小于 JL/JNGE、小于等于 JLE/JNG
ENDBR64
作用 用于标记间接跳转(通过寄存器或内存进行的 call、jmp)的合法目标地址,是 Intel CET 中 IBT(间接分支追踪)机制的一部分
- 工作原理 程序开启 CET 后,一旦执行间接跳转,处理器就进入”等待 ENDBR”状态,下一条指令必须是 ENDBR64(32位为 ENDBR32),否则触发 #CP 异常。攻击者用 ROP/JOP 把控制流劫持到函数中间时,落点不会有这条指令,从而被拦下
- 兼容性 在不支持 CET 的处理器上,ENDBR64 被当作 NOP 执行,没有任何副作用,所以编译器可以放心地在每个函数开头插入它——反汇编时看到函数以
endbr64开头,就是这个原因
BND
前缀 bnd前缀的核心思想是边界检查。当它修饰一条控制流指令(如 jmp, call, ret)时,处理器会检查跳转的目标地址是否在 BND0到 BND3这四个专用的边界寄存器所定义的合法地址范围内。如果目标地址越界,处理器会产生一个异常,这有助于及时发现并阻止由栈溢出、编程错误或恶意攻击导致的控制流劫持行为
在实际代码中,你可能会在以下场景遇到 bnd前缀:
- 动态链接:在程序的 PLT(过程链接表)中,经常能看到类似
bnd jmpq [reloc.printf]的指令。这是编译器为了增强库函数调用的安全性而插入的。 - 反汇编提示:由于
bnd前缀的编码 (F2H) 与REPNE前缀相同,一些旧版或不支持 MPX 的反汇编器可能会错误地将bnd jmp显示为repne jmp。如果你在反汇编代码中看到repne修饰控制流指令,它很可能实际上是bnd前缀
PUSH、PUSHA、PUSHF、PUSHAD
用法 PUSH source。先将栈指针减去操作数大小(64位下 rsp 减8,32位下 esp 减4),再把源操作数写入新的栈顶。与 POP 配对使用:POP 取出栈顶的值存入目的操作数,再把栈指针加回去。
- PUSHA、PUSHAD 一次性把所有通用寄存器压栈:PUSHA 压入16位寄存器(ax、cx、dx、bx、sp、bp、si、di),PUSHAD 压入32位寄存器(eax、ecx、edx、ebx、esp、ebp、esi、edi)。注意压入的 sp/esp 是指令执行之前的值。这两条指令仅在16/32位模式可用,64位模式已被移除,需要逐条 push
- PUSHF、PUSHFD、PUSHFQ 把标志寄存器压栈,分别对应16位(FLAGS)、32位(EFLAGS)、64位(RFLAGS),配对指令为 POPF、POPFD、POPFQ,常用于保存和恢复标志位状态
掌握这些常用指令后,配合调试器单步跟踪,就能读懂绝大多数 CTF PWN 题目的反汇编代码。其余复杂指令(字符串操作、SSE 等)遇到时现查即可。