汇编语言速成

指令详解

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
    5
    mov 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
    3
    call 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 funcjmp raxjmp 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

作用 用于标记间接跳转(通过寄存器或内存进行的 calljmp)的合法目标地址,是 Intel CET 中 IBT(间接分支追踪)机制的一部分

  • 工作原理 程序开启 CET 后,一旦执行间接跳转,处理器就进入”等待 ENDBR”状态,下一条指令必须是 ENDBR64(32位为 ENDBR32),否则触发 #CP 异常。攻击者用 ROP/JOP 把控制流劫持到函数中间时,落点不会有这条指令,从而被拦下
  • 兼容性 在不支持 CET 的处理器上,ENDBR64 被当作 NOP 执行,没有任何副作用,所以编译器可以放心地在每个函数开头插入它——反汇编时看到函数以 endbr64 开头,就是这个原因

BND

前缀 bnd前缀的核心思想是边界检查。当它修饰一条控制流指令(如 jmp, call, ret)时,处理器会检查跳转的目标地址是否在 BND0BND3这四个专用的边界寄存器所定义的合法地址范围内。如果目标地址越界,处理器会产生一个异常,这有助于及时发现并阻止由栈溢出、编程错误或恶意攻击导致的控制流劫持行为

在实际代码中,你可能会在以下场景遇到 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 等)遇到时现查即可。