1. 熟悉 x 86-64 汇编指令及其操作

在 x 86-64 汇编中,有一组常见的指令是每个程序员都需要熟悉的。理解这些指令的工作原理对阅读和理解反汇编代码非常重要。指令分为多种类型,主要包括数据传输、算术、逻辑、跳转、条件控制等。

常见指令及其意义

  • mov:将数据从源复制到目标。
    • 例子:mov %rax, %rbx%rax 的值复制到 %rbx 中。
  • add:将两个数相加,并将结果存储在目标操作数中。
    • 例子:add %rax, %rbx%rax 的值加到 %rbx 中,结果保存在 %rbx
  • sub:从目标操作数中减去源操作数。
    • 例子:sub %rax, %rbx%rax 的值从 %rbx 中减去,结果保存在 %rbx
  • cmp:比较两个操作数,结果存储在标志寄存器中(不改变原始操作数)。
    • 例子:cmp %rax, %rbx 比较 %rax%rbx (左减右),并设置标志位,用于后续的条件跳转。
  • jmp:无条件跳转到指定的内存地址。
    • 例子:jmp *%rax 跳转到 %rax 中存储的地址。
  • je / jne:条件跳转指令,根据前一条 cmp 指令的结果进行跳转(相等/不相等)。
    • 例子:je label 如果相等,跳转到 label;否则继续执行。
  • push / pop:堆栈操作,push 将数据压入栈顶,pop 将数据从栈顶弹出。
    • 例子:push %rax%rax 的值压入堆栈;pop %rbx 从堆栈弹出一个值到 %rbx
  • call:调用函数。将返回地址压入堆栈并跳转到函数的地址。
    • 例子:call *%rax 调用 %rax 指定的地址函数。
  • ret:返回函数。将堆栈顶的返回地址弹出并跳转到该地址。
    • 例子:ret 执行函数返回。

常用的条件跳转指令

常用的条件跳转指令

条件跳转指令是 x 86 汇编中非常重要的部分,它们根据标志寄存器的状态执行跳转。标志寄存器由之前的比较指令(如 cmp)或算术指令(如 add, sub)设置,用于决定跳转的条件是否成立。不同条件跳转指令适用于不同的场景,常见的条件跳转指令如下:

1. je(Jump if Equal)/ jz(Jump if Zero)

  • 条件:如果比较结果相等(即零标志位 ZF=1),则跳转。
  • 用途:通常在两个值相等时使用。
  • 例子

    cmp %rax, %rbx   ; 比较 %rax 和 %rbx
    je equal_label   ; 如果相等,跳转到 equal_label
    

    如果 %rax%rbx 相等,则跳转到 equal_label 位置继续执行。

2. jne(Jump if Not Equal)/ jnz(Jump if Not Zero)

  • 条件:如果比较结果不相等(即零标志位 ZF=0),则跳转。
  • 用途:用于处理两个值不相等的情况。
  • 例子

    cmp %rax, %rbx   ; 比较 %rax 和 %rbx
    jne not_equal_label ; 如果不相等,跳转到 not_equal_label
    

    如果 %rax%rbx 不相等,则跳转到 not_equal_label

3. jg(Jump if Greater)/ jnle(Jump if Not Less or Equal)

  • 条件:如果比较结果表示第一个操作数大于第二个(即符号位 SF=0 和零标志位 ZF=0),则跳转。
  • 用途:用于有符号比较时,第一个数大于第二个数时跳转。
  • 例子

    cmp %rax, %rbx   ; 比较 %rax 和 %rbx
    jg greater_label ; 如果 %rax 大于 %rbx,跳转到 greater_label
    

    如果 %rax 大于 %rbx,则跳转到 greater_label

4. jl(Jump if Less)/ jnge(Jump if Not Greater or Equal)

  • 条件:如果第一个操作数小于第二个(符号位 SF=1 且零标志位 ZF=0),则跳转。
  • 用途:用于有符号比较,第一个数小于第二个数时跳转。
  • 例子

    cmp %rax, %rbx   ; 比较 %rax 和 %rbx
    jl less_label    ; 如果 %rax 小于 %rbx,跳转到 less_label
    

    如果 %rax 小于 %rbx,则跳转到 less_label

5. jge(Jump if Greater or Equal)

  • 条件:如果第一个操作数大于或等于第二个(即符号位 SF=0 或零标志位 ZF=1),则跳转。
  • 用途:用于有符号比较,表示大于或等于时跳转。
  • 例子

    cmp %rax, %rbx   ; 比较 %rax 和 %rbx
    jge ge_label     ; 如果 %rax 大于或等于 %rbx,跳转到 ge_label
    

    如果 %rax 大于或等于 %rbx,则跳转到 ge_label

6. jle(Jump if Less or Equal)

  • 条件:如果第一个操作数小于或等于第二个(符号位 SF=1 或零标志位 ZF=1),则跳转。
  • 用途:用于有符号比较,表示小于或等于时跳转。
  • 例子

    cmp %rax, %rbx   ; 比较 %rax 和 %rbx
    jle le_label     ; 如果 %rax 小于或等于 %rbx,跳转到 le_label
    

    如果 %rax 小于或等于 %rbx,则跳转到 le_label

7. ja(Jump if Above)/ jnbe(Jump if Not Below or Equal)

  • 条件:如果第一个操作数严格大于第二个(零标志位 ZF=0 且进位标志位 CF=0),则跳转(无符号比较)。
  • 用途:用于无符号数比较时,第一个数大于第二个数。
  • 例子

    cmp %rax, %rbx   ; 无符号比较 %rax 和 %rbx
    ja above_label   ; 如果 %rax 大于 %rbx,跳转到 above_label
    

    如果 %rax 严格大于 %rbx,则跳转到 above_label

8. jb(Jump if Below)/ jc(Jump if Carry)

  • 条件:如果第一个操作数小于第二个(进位标志位 CF=1),则跳转(无符号比较)。
  • 用途:用于无符号数比较时,第一个数小于第二个数。
  • 例子

    cmp %rax, %rbx   ; 无符号比较 %rax 和 %rbx
    jb below_label   ; 如果 %rax 小于 %rbx,跳转到 below_label
    

    如果 %rax 小于 %rbx,则跳转到 below_label

9. jbe(Jump if Below or Equal)

  • 条件:如果第一个操作数小于或等于第二个(零标志位 ZF=1 或进位标志位 CF=1),则跳转(无符号比较)。
  • 用途:用于无符号数比较时,第一个数小于或等于第二个数。
  • 例子

    cmp %rax, %rbx   ; 无符号比较 %rax 和 %rbx
    jbe be_label     ; 如果 %rax 小于或等于 %rbx,跳转到 be_label
    

    如果 %rax 小于或等于 %rbx,则跳转到 be_label

10. jno(Jump if No Overflow)/ jo(Jump if Overflow)

  • 条件:根据溢出标志位 OF 的状态跳转。
    • jno:如果没有溢出(OF=0),则跳转。
    • jo:如果发生溢出(OF=1),则跳转。

    例子

    add %rax, %rbx   ; 执行加法,可能产生溢出
    jo overflow_label ; 如果产生溢出,跳转到 overflow_label
    

    如果加法操作导致溢出,则跳转到 overflow_label

11.setg %al ; 如果上一条 cmp 指令结果是“greater”,则设置 %al = 1,否则 %al = 0


举例说明

假设我们有一个简单的程序,它比较两个数字并根据结果决定跳转到不同的标签:

    mov $5, %rax      ; 将 5 存入寄存器 %rax
    mov $3, %rbx      ; 将 3 存入寄存器 %rbx
    cmp %rbx, %rax    ; 比较 %rax 和 %rbx (%rax - %rbx)
    je equal_label    ; 如果 %rax == %rbx,跳转到 equal_label
    jg greater_label  ; 如果 %rax > %rbx,跳转到 greater_label
    jl less_label     ; 如果 %rax < %rbx,跳转到 less_label
    
equal_label:
    ; 执行 %rax 和 %rbx 相等时的逻辑
    ret

greater_label:
    ; 执行 %rax 大于 %rbx 时的逻辑
    ret

less_label:
    ; 执行 %rax 小于 %rbx 时的逻辑
    ret

这个代码片段首先比较寄存器 %rax%rbx 中的值。根据比较结果,它会跳转到不同的标签:

  • 如果它们相等,则跳转到 equal_label
  • 如果 %rax 大于 %rbx,则跳转到 greater_label
  • 如果 %rax 小于 %rbx,则跳转到 less_label

AT&T 格式注意点:

在 AT&T 汇编格式中,源操作数在前,目标操作数在后。这是与 Intel 汇编格式最主要的区别。你需要调整阅读习惯。例如:

  • AT&T 格式:mov %eax, %ebx(把 %eax 的值复制到 %ebx
  • Intel 格式:mov ebx, eax(目标在前,源在后)

在 AT&T 格式中,立即数$ 开头,寄存器以 % 开头。例子:

  • $5 表示立即数 5
  • %rax 表示寄存器 RAX
  • (%rbx) 表示内存地址指向 %rbx 中存储的值

2. 掌握寄存器命名规则

x 86-64 架构有 16 个通用寄存器,每个寄存器有 64 位、32 位、16 位、8 位的子寄存器。了解它们的命名方式有助于快速理解代码。

主要寄存器及其用途

p16

  1. 通用寄存器
    • rax:累加器,通常用于函数返回值。
    • rbx:基址寄存器,可以用于存储数据或指针。
    • rcx:循环计数器,通常用于循环操作。
    • rdx:数据寄存器,常用于输入/输出操作或乘除法的中间结果。
    • rsi:源索引寄存器,用于字符串操作和函数参数传递。
    • rdi:目标索引寄存器,用于函数参数传递。
    • rsp:堆栈指针,管理函数调用时的堆栈操作。
    • rbp:基址指针,常用于保存栈帧的起始位置。
  2. 子寄存器: 每个 64 位寄存器有不同的子寄存器,用于不同大小的数据:
    • rax:64 位寄存器
    • eax:32 位寄存器
    • ax:16 位寄存器
    • al:8 位寄存器

例如,mov $5, %rax5 存储在 64 位寄存器 %rax 中,mov $5, %eax 仅影响寄存器的低 32 位。

寄存器的常见使用规则

[!TIP] 有些寄存器在 GDB 中是无法通过 p 命令直接查看其内容的,尤其是那些特殊用途或状态寄存器(如标志寄存器、控制寄存器等)。当 GDB 不支持直接访问这些寄存器的值时,输出可能会显示为 void 或提示不能打印该值。

  • 函数调用的寄存器:根据 System V AMD64 ABI 规范,函数参数依次通过 rdi, rsi, rdx, rcx, r8, r9 传递。
  • 局部变量:通常通过堆栈保存,rbprsp 用于管理函数栈帧。

3. 分块阅读代码

[!NOTE] 以 . 开头的行都是指导汇编器和链接器工作的伪指令,这些可以忽略。 函数通常从 call 指令调用,并以 ret 返回。

函数边界

  • 函数入口:函数通常从 call 指令调用,并以 ret 返回。可以通过定位这些指令来识别一个函数的起始和结束。
  • 函数栈帧:函数通常在进入时通过 push %rbp 保存之前的基址指针,并通过 mov %rsp, %rbp 设置新的栈帧。

基本块和跳转指令

  • 基本块:基本块是指一段没有跳转的连续代码,通常从某个入口点开始,直到一个跳转或函数调用结束。
  • 条件跳转:如 je, jne, jg, jl 等,它们依赖于先前 cmp 指令的结果,可以快速识别逻辑分支。
  • 无条件跳转jmp 用于循环或无条件跳转。

通过识别函数入口、出口以及跳转逻辑,你可以将代码划分成较小的块来逐一分析。

4. 阅读顺序与可以忽略的部分

阅读顺序

  • 从高层逻辑入手:首先从函数入口和调用分析整体逻辑。理解主函数的作用后,再深入分析细节。
  • 关注关键操作:跳过无关的操作,如大量的 nop(无操作指令)和编译器自动生成的无效代码。专注于条件判断、函数调用、栈操作等。

可以忽略的内容

  • 无关的寄存器操作:某些寄存器保存的是临时变量或不重要的中间值,这些可以先跳过,等需要时再回头看。
  • 调试符号:反汇编代码可能包含调试信息或 padding,可以忽略不影响主逻辑的部分。