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 位的子寄存器。了解它们的命名方式有助于快速理解代码。
主要寄存器及其用途

- 通用寄存器:
rax:累加器,通常用于函数返回值。rbx:基址寄存器,可以用于存储数据或指针。rcx:循环计数器,通常用于循环操作。rdx:数据寄存器,常用于输入/输出操作或乘除法的中间结果。rsi:源索引寄存器,用于字符串操作和函数参数传递。rdi:目标索引寄存器,用于函数参数传递。rsp:堆栈指针,管理函数调用时的堆栈操作。rbp:基址指针,常用于保存栈帧的起始位置。
- 子寄存器: 每个 64 位寄存器有不同的子寄存器,用于不同大小的数据:
rax:64 位寄存器eax:32 位寄存器ax:16 位寄存器al:8 位寄存器
例如,mov $5, %rax 将 5 存储在 64 位寄存器 %rax 中,mov $5, %eax 仅影响寄存器的低 32 位。
寄存器的常见使用规则
[!TIP] 有些寄存器在 GDB 中是无法通过
p命令直接查看其内容的,尤其是那些特殊用途或状态寄存器(如标志寄存器、控制寄存器等)。当 GDB 不支持直接访问这些寄存器的值时,输出可能会显示为void或提示不能打印该值。
- 函数调用的寄存器:根据
System V AMD64 ABI规范,函数参数依次通过rdi,rsi,rdx,rcx,r8,r9传递。 - 局部变量:通常通过堆栈保存,
rbp和rsp用于管理函数栈帧。
3. 分块阅读代码
[!NOTE] 以
.开头的行都是指导汇编器和链接器工作的伪指令,这些可以忽略。 函数通常从call指令调用,并以ret返回。
函数边界
- 函数入口:函数通常从
call指令调用,并以ret返回。可以通过定位这些指令来识别一个函数的起始和结束。 - 函数栈帧:函数通常在进入时通过
push %rbp保存之前的基址指针,并通过mov %rsp, %rbp设置新的栈帧。
基本块和跳转指令
- 基本块:基本块是指一段没有跳转的连续代码,通常从某个入口点开始,直到一个跳转或函数调用结束。
- 条件跳转:如
je,jne,jg,jl等,它们依赖于先前cmp指令的结果,可以快速识别逻辑分支。 - 无条件跳转:
jmp用于循环或无条件跳转。
通过识别函数入口、出口以及跳转逻辑,你可以将代码划分成较小的块来逐一分析。
4. 阅读顺序与可以忽略的部分
阅读顺序
- 从高层逻辑入手:首先从函数入口和调用分析整体逻辑。理解主函数的作用后,再深入分析细节。
- 关注关键操作:跳过无关的操作,如大量的
nop(无操作指令)和编译器自动生成的无效代码。专注于条件判断、函数调用、栈操作等。
可以忽略的内容
- 无关的寄存器操作:某些寄存器保存的是临时变量或不重要的中间值,这些可以先跳过,等需要时再回头看。
- 调试符号:反汇编代码可能包含调试信息或 padding,可以忽略不影响主逻辑的部分。