第 3 章:程序的机器级表示
主题:程序的机器级表示 (Machine-Level Representation of Programs)
目标:能读懂 x86-64 汇编,理解 C 程序如何映射到指令、寄存器、栈、控制流、过程调用和浮点运算。
1. 从 C 到机器代码
flowchart LR
A["hello.c<br/>C 源程序"] --> B["预处理<br/>cpp"]
B --> C["编译<br/>gcc -S"]
C --> D["hello.s<br/>汇编代码"]
D --> E["汇编<br/>as"]
E --> F["hello.o<br/>可重定位目标文件"]
F --> G["链接<br/>ld"]
G --> H["可执行文件"]
常用命令:
| 命令 | 作用 |
|---|---|
gcc -Og -S a.c | 生成调试友好的汇编 .s |
gcc -O1/-O2 -S a.c | 生成更优化的汇编,可能出现 cmov 等指令 |
gcc -c a.c | 生成目标文件 .o |
objdump -d a.out | 反汇编可执行文件 |
gdb ./a.out | 调试程序 |
注意:CSAPP 多用 AT&T 汇编语法,操作数顺序是
源, 目的,例如movq %rax, %rbx表示%rbx = %rax。
2. x86-64 寄存器速查
| 64 位 | 32 位 | 16 位 | 8 位 | 常见用途 |
|---|---|---|---|---|
%rax | %eax | %ax | %al | 返回值、临时值 |
%rbx | %ebx | %bx | %bl | 被调用者保存 |
%rcx | %ecx | %cx | %cl | 第 4 参数、移位计数 |
%rdx | %edx | %dx | %dl | 第 3 参数、乘除辅助 |
%rsi | %esi | %si | %sil | 第 2 参数 |
%rdi | %edi | %di | %dil | 第 1 参数 |
%rbp | %ebp | %bp | %bpl | 栈帧基址,可被优化掉 |
%rsp | %esp | %sp | %spl | 栈顶指针 |
%r8 | %r8d | %r8w | %r8b | 第 5 参数 |
%r9 | %r9d | %r9w | %r9b | 第 6 参数 |
%r10-%r11 | - | - | - | 调用者保存 |
%r12-%r15 | - | - | - | 被调用者保存 |
寄存器保存规则:
| 类型 | 寄存器 | 谁负责恢复 |
|---|---|---|
| 调用者保存 (Caller-Saved) | %rax, %rdi, %rsi, %rdx, %rcx, %r8-%r11 | 调用函数前的调用者 |
| 被调用者保存 (Callee-Saved) | %rbx, %rbp, %r12-%r15 | 被调用函数 |
| 特殊 | %rsp | 始终指向栈顶 |
3. 操作数与寻址方式
| 形式 | 含义 | 示例 |
|---|---|---|
$Imm | 立即数 | $0x10 |
Reg | 寄存器 | %rax |
Imm | 绝对地址 | 0x4005d0 |
(Reg) | 内存 [Reg] | (%rax) |
Imm(Reg) | 内存 [Reg + Imm] | 8(%rbp) |
(Rb,Ri) | 内存 [Rb + Ri] | (%rax,%rdx) |
Imm(Rb,Ri,s) | 内存 [Rb + Ri*s + Imm] | 8(%rbx,%rcx,4) |
地址计算公式:
地址 = Imm + Rb + Ri * s
s 只能是 1、2、4、8
leaq 是重点:
| 指令 | 本质 | 常见用途 |
|---|---|---|
leaq S, D | 把地址表达式的值写入 D | 地址计算、整数线性表达式计算 |
示例:
leaq 7(%rdi,%rdi,4), %rax
等价于:
rax = 5 * rdi + 7;
4. 数据传送指令
| 指令 | 含义 |
|---|---|
movb/movw/movl/movq | 传送 1/2/4/8 字节 |
movzbl | 零扩展:小整数转大整数,高位补 0 |
movsbl | 符号扩展:小整数转大整数,高位补符号位 |
pushq S | rsp -= 8; M[rsp] = S |
popq D | D = M[rsp]; rsp += 8 |
易错点:写入 32 位寄存器会自动把对应 64 位寄存器高 32 位清零。
例如movl $0, %eax会让%rax变成 0。
5. 算术与逻辑操作
| 指令 | C 类比 | 说明 |
|---|---|---|
addq S, D | D += S | 加法 |
subq S, D | D -= S | 减法 |
imulq S, D | D *= S | 有符号乘法 |
salq/shlq k, D | D <<= k | 左移 |
sarq k, D | D >>= k | 算术右移,保留符号 |
shrq k, D | D >>= k | 逻辑右移,高位补 0 |
xorq S, D | D ^= S | 常用 xor %eax,%eax 清零 |
andq S, D | D &= S | 位与 |
orq S, D | D |= S | 位或 |
incq D | D++ | 不影响 CF |
decq D | D-- | 不影响 CF |
negq D | D = -D | 取负 |
notq D | D = ~D | 按位取反 |
移位计数可以是立即数,也可以放在 %cl 中。
6. 条件码
| 条件码 | 名称 | 含义 |
|---|---|---|
CF | Carry Flag | 无符号溢出/借位 |
ZF | Zero Flag | 结果为 0 |
SF | Sign Flag | 结果为负 |
OF | Overflow Flag | 有符号溢出 |
常见设置方式:
| 指令 | 效果 |
|---|---|
cmpq S1, S2 | 计算 S2 - S1,只设置条件码 |
testq S1, S2 | 计算 S2 & S1,只设置条件码 |
add/sub/and/xor | 计算结果并设置条件码 |
leaq | 不设置条件码 |
有符号与无符号比较:
| C 表达式 | 有符号跳转 | 无符号跳转 |
|---|---|---|
a == b | je | je |
a != b | jne | jne |
a < b | jl | jb |
a <= b | jle | jbe |
a > b | jg | ja |
a >= b | jge | jae |
7. 控制流
7.1 条件跳转
flowchart TD
A["cmp/test 设置条件码"] --> B{"条件成立?"}
B -- 是 --> C["跳转到目标标签"]
B -- 否 --> D["继续顺序执行"]
典型 if-else:
if (x < y)
result = y - x;
else
result = x - y;
可能翻译为:
cmpq %rsi, %rdi
jge .L2
movq %rsi, %rax
subq %rdi, %rax
ret
.L2:
movq %rdi, %rax
subq %rsi, %rax
ret
7.2 条件传送
条件传送 (Conditional Move) 用 cmovXX 避免分支。
cmpq %rsi, %rdi
cmovl %rdx, %rax
适合条件:
适合 cmov | 不适合 cmov |
|---|---|
| 两边表达式都很简单 | 分支有副作用 |
| 分支预测困难 | 某个分支计算代价很大 |
| 两边都可以安全执行 | 可能触发异常,如非法内存访问 |
注意:
-Og偏向调试友好,常保留跳转;-O1/-O2更容易生成cmov。
7.3 循环
循环常被转成 goto 风格。
flowchart TD
A["初始化"] --> B{"测试条件"}
B -- 假 --> E["退出"]
B -- 真 --> C["循环体"]
C --> D["更新"]
D --> B
do-while 最接近汇编结构,因为先执行循环体,再判断是否回跳。
7.4 switch
switch 可能使用跳转表 (Jump Table):
flowchart LR
A["索引 i"] --> B["边界检查"]
B --> C["跳转表"]
C --> D["case 0"]
C --> E["case 1"]
C --> F["case 2"]
C --> G["default"]
适合跳转表的情况:
| 条件 | 原因 |
|---|---|
| case 值比较密集 | 表大小可控 |
| case 数量较多 | 比连续比较更快 |
| 范围可提前检查 | 防止越界跳转 |
8. 过程调用
8.1 参数传递
前 6 个整数或指针参数:
| 参数序号 | 寄存器 |
|---|---|
| 1 | %rdi |
| 2 | %rsi |
| 3 | %rdx |
| 4 | %rcx |
| 5 | %r8 |
| 6 | %r9 |
第 7 个及之后的参数放到栈上。
返回值:
| 返回类型 | 位置 |
|---|---|
| 整数/指针 | %rax |
| 浮点数 | %xmm0 |
8.2 栈帧结构
flowchart TB
A["高地址"] --> B["调用者栈帧"]
B --> C["参数 7、8..."]
C --> D["返回地址"]
D --> E["保存的寄存器"]
E --> F["局部变量"]
F --> G["临时空间"]
G --> H["低地址<br/>%rsp"]
调用流程:
| 指令 | 等价行为 |
|---|---|
call label | 压入返回地址,然后跳转 |
ret | 弹出返回地址,然后跳转 |
重点:栈向低地址增长,
push会减小%rsp,pop会增大%rsp。
8.3 递归
递归本质是多个栈帧共存:
flowchart TB
A["fact(3) 栈帧"] --> B["fact(2) 栈帧"]
B --> C["fact(1) 栈帧"]
C --> D["逐层返回"]
每一层调用都有自己的局部变量、返回地址和保存寄存器。
9. 数组、指针与结构体
9.1 数组
C 数组访问:
A[i]
地址计算:
&A[i] = A + i * sizeof(A[0])
二维数组:
int A[R][C];
地址:
&A[i][j] = A + sizeof(int) * (i * C + j)
9.2 结构体
结构体字段通过固定偏移访问:
struct S {
int x;
long y;
};
可能布局:
| 字段 | 偏移 | 大小 |
|---|---|---|
x | 0 | 4 |
| 填充 | 4 | 4 |
y | 8 | 8 |
对齐 (Alignment) 会插入填充字节,结构体大小通常是最大对齐要求的倍数。
9.3 union
联合体 (Union) 的所有字段共享同一段内存。
| 特点 | 说明 |
|---|---|
| 大小 | 等于最大字段大小,外加对齐 |
| 偏移 | 所有字段通常从 0 开始 |
| 风险 | 用错误字段解释数据会产生低层表示相关行为 |
10. 浮点代码
现代 x86-64 通常使用 SSE/AVX 指令和 %xmm 寄存器处理 float、double,而不是使用通用寄存器。更早的 x87 浮点栈主要用于兼容旧代码和处理部分 long double 场景。
本节调用约定以 Linux 上的 System V AMD64 ABI 为准;Windows x64 的参数寄存器分配规则不同。
10.1 浮点寄存器与数据格式
XMM 寄存器宽 128 位,YMM 寄存器宽 256 位。标量指令只计算寄存器低位的一个元素,打包指令则并行计算多个元素。
| 后缀 | 数据形式 | XMM 中的有效元素 |
|---|---|---|
ss | Scalar Single,标量 float | 低 32 位的 1 个单精度数 |
sd | Scalar Double,标量 double | 低 64 位的 1 个双精度数 |
ps | Packed Single,打包 float | 4 个单精度数 |
pd | Packed Double,打包 double | 2 个双精度数 |
例如,addss 是标量单精度加法,addsd 是标量双精度加法,addps 和 addpd 则执行向量化加法。
10.2 数据传送与算术指令
| 指令族 | 作用 |
|---|---|
movss / movsd | 传送标量单精度 / 双精度数 |
addss / addsd | 浮点加法 |
subss / subsd | 浮点减法 |
mulss / mulsd | 浮点乘法 |
divss / divsd | 浮点除法 |
sqrtss / sqrtsd | 浮点平方根 |
xorps / xorpd | 按位异或,常用于把 XMM 寄存器清零 |
传统 SSE 指令通常采用两操作数形式,目的寄存器同时也是一个源操作数:
subsd %xmm1, %xmm0 # xmm0 = xmm0 - xmm1
AVX 指令通常在名称前增加 v,并使用三操作数形式:
vsubsd %xmm1, %xmm0, %xmm2 # xmm2 = xmm0 - xmm1
AT&T 语法仍然遵循“源在前、目的在后”。对于
vsubsd S1, S2, D,结果是D = S2 - S1,别把两个源操作数看反。
x86 浮点算术指令通常不能直接使用浮点立即数,编译器会把常量放到只读数据区,再通过 RIP 相对寻址加载:
movsd .LC0(%rip), %xmm0
10.3 整数与浮点数转换
| 指令族 | 转换方向 |
|---|---|
cvtsi2ss / cvtsi2sd | 有符号整数转 float / double |
cvttss2si / cvttsd2si | float / double 截断后转有符号整数 |
cvtss2sd | float 转 double |
cvtsd2ss | double 转 float,可能发生舍入 |
cvtt 中的第二个 t 表示 truncate,即向零截断,与 C 的浮点数转整数规则一致。整数源或目的的宽度还会由具体指令后缀或操作数宽度决定。
当浮点值为 NaN、无穷或超出目标整数范围时,不要根据某台机器的转换结果推断可移植的 C 行为;这类转换可能超出语言允许的表示范围。
10.4 浮点比较与 NaN
ucomiss 和 ucomisd 分别比较单精度、双精度浮点数,并设置整数条件码 ZF、PF 和 CF。在 AT&T 语法中,ucomisd S1, S2 比较的是 S2 与 S1。
| 比较结果 | ZF | PF | CF |
|---|---|---|---|
S2 > S1 | 0 | 0 | 0 |
S2 < S1 | 0 | 0 | 1 |
S2 == S1 | 1 | 0 | 0 |
| 无序(至少一个操作数为 NaN) | 1 | 1 | 1 |
浮点比较比整数比较多出“无序”状态,因此编译器必须正确处理 PF,或选择能自然排除无序情况的跳转组合:
- 与 NaN 做
<、<=、>、>=、==比较均为假。 - 与 NaN 做
!=比较为真。 - 仅根据
ZF = 1判断浮点相等会把 NaN 的无序状态误当成相等。
10.5 浮点调用约定
System V AMD64 使用两组相互独立的参数寄存器:整数和指针依次使用 %rdi 至 %r9,浮点参数依次使用 %xmm0 至 %xmm7。
| 内容 | 位置 |
|---|---|
| 前 8 个可用的浮点参数 | %xmm0 至 %xmm7 |
| 浮点返回值 | %xmm0 |
| 超出寄存器容量的参数 | 栈 |
%xmm0-%xmm15 | 调用者保存 |
混合参数分别占用各自类别的寄存器。例如:
double f(int i, double x, float y);
在 System V AMD64 下,i 位于 %edi,x 位于 %xmm0,y 位于 %xmm1,并不会因为 i 是第一个参数就让 x 改用 %xmm1。
10.6 从 C 代码还原浮点汇编
double scaled_delta(double x, double y, int scale)
{
return (x - y) * (double)scale;
}
一种可能的 SSE 汇编是:
scaled_delta:
subsd %xmm1, %xmm0 # x - y
pxor %xmm1, %xmm1 # xmm1 清零
cvtsi2sdl %edi, %xmm1 # (double) scale
mulsd %xmm1, %xmm0 # (x - y) * scale
ret # 结果位于 xmm0
读浮点汇编时按以下顺序检查:
- 根据
ss、sd、ps、pd判断数据类型和标量/向量形式。 - 查看
%xmm0-%xmm7,确定浮点参数及返回值。 - 遇到
cvt指令时确认转换方向、整数宽度和舍入方式。 - 遇到
ucomis时同时考虑 NaN 对PF的影响。 - 区分 SSE 两操作数和 AVX 三操作数形式,尤其注意减法、除法的源操作数顺序。
GDB 中可查看 XMM 寄存器的不同解释:
info registers xmm0 xmm1
p $xmm0.v2_double[0]
p $xmm1.v4_float[0]
info registers mxcsr
MXCSR 保存 SSE/AVX 浮点运算的舍入模式、异常标志和异常屏蔽位。
11. 内存越界与攻击
栈缓冲区溢出:
flowchart TB
A["局部数组 buf"] --> B["保存的寄存器"]
B --> C["返回地址"]
C --> D["调用者栈帧"]
E["写入超过 buf 边界"] --> B
E --> C
风险:
| 问题 | 后果 |
|---|---|
| 数组越界写 | 覆盖局部变量、保存寄存器、返回地址 |
| 不检查输入长度 | 栈溢出 |
| 执行栈数据 | 代码注入 |
| 修改返回地址 | 控制流劫持 |
常见防护:
| 机制 | 作用 |
|---|---|
| 栈随机化 (Stack Randomization) | 地址难预测 |
| 栈保护 (Stack Protector) | 检测返回地址附近是否被破坏 |
| 栈不可执行 (NX) | 阻止执行栈上注入代码 |
| 地址空间布局随机化 (ASLR) | 随机化代码/库/栈地址 |
12. 读汇编的推荐顺序
flowchart TD
A["看函数名和参数寄存器"] --> B["找返回值 %rax / %xmm0"]
B --> C["识别栈帧和局部变量"]
C --> D["识别 cmp/test 与跳转"]
D --> E["还原 if/loop/switch"]
E --> F["识别数组和结构体偏移"]
速查步骤:
- 先看函数入口:参数在哪些寄存器。
- 再看
%rax或%xmm0:整数/指针与浮点返回值如何形成。 - 遇到
cmp/test + jXX:还原条件判断。 - 遇到
leaq:优先当作算术表达式或地址计算。 - 遇到
call:检查调用前的通用参数寄存器和 XMM 参数寄存器。 - 遇到
(%rdi,%rax,4):联想到数组下标。 - 遇到固定偏移如
8(%rdi):联想到结构体字段。
13. GDB 调试机器级程序
GDB 适合把汇编、寄存器、栈和内存状态连起来看。
13.1 编译建议
自己写的小程序建议这样编译:
gcc -Og -g -fno-omit-frame-pointer -no-pie test.c -o test
| 选项 | 作用 |
|---|---|
-Og | 保留较清晰的源代码结构,适合调试 |
-g | 生成调试信息 |
-fno-omit-frame-pointer | 保留 %rbp 栈帧指针,方便观察栈帧 |
-no-pie | 关闭位置无关可执行文件,地址更稳定 |
启动:
gdb -q ./test
13.2 运行控制
| 命令 | 简写 | 作用 |
|---|---|---|
run | r | 启动程序 |
run arg1 arg2 | r arg1 arg2 | 带命令行参数启动 |
start | - | 停在 main 开始处 |
continue | c | 继续运行到下一个断点 |
next | n | 单步执行 C 语句,不进入函数 |
step | s | 单步执行 C 语句,进入函数 |
nexti | ni | 单步执行一条机器指令,不进入调用 |
stepi | si | 单步执行一条机器指令,可进入调用 |
finish | - | 执行到当前函数返回 |
quit | q | 退出 GDB |
看汇编时优先用
si/ni,看 C 源码时再用s/n。
13.3 断点
| 命令 | 作用 |
|---|---|
break main | 在函数入口断点 |
break file.c:12 | 在源码行断点 |
break *0x401136 | 在机器地址断点 |
break *phase_1 | 在函数入口地址断点 |
info breakpoints | 查看断点 |
delete 1 | 删除编号为 1 的断点 |
disable 1 / enable 1 | 禁用/启用断点 |
condition 1 x == 0 | 给断点 1 加条件 |
Bomb Lab 常用:
break main
break phase_1
break explode_bomb
run
给
explode_bomb下断点,可以在炸弹真正退出前停住,便于回看调用路径。
13.4 查看代码与反汇编
| 命令 | 作用 |
|---|---|
list | 查看源码 |
disassemble main | 反汇编函数 |
disassemble /r main | 同时显示机器码字节 |
x/10i $pc | 从当前程序计数器开始显示 10 条指令 |
layout asm | TUI 汇编窗口 |
layout regs | TUI 寄存器窗口 |
set disassembly-flavor att | 使用 AT&T 语法 |
set disassembly-flavor intel | 使用 Intel 语法 |
当前执行位置:
x/i $pc
$pc 在 x86-64 上通常对应 %rip。
13.5 查看寄存器
| 命令 | 作用 |
|---|---|
info registers | 查看所有通用寄存器 |
info registers rax rdi rsi | 查看指定寄存器 |
print/x $rax | 以十六进制打印 %rax |
print/d $rax | 以十进制打印 %rax |
print/t $rax | 以二进制打印 %rax |
set $rax = 0 | 修改寄存器值 |
参数寄存器速查:
| 参数 | 寄存器 |
|---|---|
| 第 1 个 | %rdi |
| 第 2 个 | %rsi |
| 第 3 个 | %rdx |
| 第 4 个 | %rcx |
| 第 5 个 | %r8 |
| 第 6 个 | %r9 |
整数和指针返回值通常看 %rax,浮点返回值通常看 %xmm0。
13.6 查看内存
x 命令格式:
x/NFU 地址
含义:
| 字段 | 含义 | 常用值 |
|---|---|---|
N | 显示多少个单元 | 1, 4, 16, 32 |
F | 显示格式 | x 十六进制, d 十进制, s 字符串, i 指令 |
U | 单元大小 | b 1 字节, h 2 字节, w 4 字节, g 8 字节 |
常用命令:
| 命令 | 作用 |
|---|---|
x/16xb $rsp | 从栈顶看 16 个字节 |
x/8gx $rsp | 从栈顶看 8 个 8 字节值 |
x/s $rdi | 把 %rdi 指向的内存当字符串打印 |
x/10i $rip | 从当前指令开始看 10 条指令 |
x/wx 0x404040 | 查看地址处 4 字节十六进制值 |
示例:检查函数第一个字符串参数。
info registers rdi
x/s $rdi
13.7 查看栈与调用链
| 命令 | 作用 |
|---|---|
backtrace | 查看调用链 |
bt | backtrace 简写 |
frame 1 | 切换到调用链第 1 帧 |
info frame | 查看当前栈帧信息 |
info args | 查看当前函数参数 |
info locals | 查看局部变量 |
x/8gx $rsp | 查看栈顶附近内容 |
x/8gx $rbp-0x20 | 查看当前栈帧局部区域 |
栈观察重点:
flowchart TB
A["高地址"] --> B["调用者栈帧"]
B --> C["返回地址"]
C --> D["保存的 %rbp"]
D --> E["局部变量/临时空间"]
E --> F["低地址:%rsp"]
栈向低地址增长。
call会把返回地址压栈,ret会从栈顶弹出返回地址。
13.8 条件码与分支调试
GDB 不直接把 ZF/SF/OF/CF 作为普通变量展示,但可以看 %eflags。
| 命令 | 作用 |
|---|---|
info registers eflags | 查看条件码寄存器 |
x/i $pc | 查看当前将执行的跳转指令 |
si | 执行一条指令,观察是否跳转 |
调试 cmp/test + jXX 的顺序:
- 停在
cmp或test前。 - 查看参与比较的寄存器或内存。
- 执行
si,让条件码被设置。 - 查看
eflags。 - 再执行
si,观察jXX是否改变%rip。
13.9 Bomb Lab 基本流程
flowchart TD
A["objdump/gdb 查看 phase 函数"] --> B["给 phase_i 下断点"]
B --> C["run 输入文件"]
C --> D["观察参数寄存器和字符串"]
D --> E["单步执行分支"]
E --> F["推出输入约束"]
F --> G["写入答案文件"]
推荐启动方式:
gdb -q ./bomb
常用命令:
set disassembly-flavor att
break main
break explode_bomb
break phase_1
run answers.txt
查看当前 phase 的输入:
info registers rdi
x/s $rdi
查看比较字符串:
x/s 0x402400
具体地址要以当前反汇编结果为准。
13.10 常用自动显示
display 可以让 GDB 每次暂停时自动打印关心的信息。
| 命令 | 作用 |
|---|---|
display/i $pc | 每次停下显示当前指令 |
display/x $rax | 每次停下显示 %rax |
display/x $rsp | 每次停下显示栈顶地址 |
undisplay 1 | 取消编号为 1 的自动显示 |
适合写入 tools/gdb/csapp.gdb:
set disassembly-flavor att
set pagination off
display/i $pc
使用:
gdb -q -x tools/gdb/csapp.gdb ./bomb
14. 常见坑
| 坑 | 正确理解 |
|---|---|
| AT&T 操作数顺序看反 | movq S, D 是 D = S |
cmpq a, b 看反 | 比较的是 b - a |
以为 leaq 会访存 | leaq 只算地址,不读内存 |
| 混淆有符号/无符号跳转 | jl/jg 是有符号,jb/ja 是无符号 |
| 忘记栈向低地址增长 | push 让 %rsp 减小 |
认为 -Og 会充分优化 | -Og 偏调试,-O1/-O2 才更激进 |
| 忽略 32 位写寄存器清零 | 写 %eax 会清空 %rax 高 32 位 |
混淆 step 和 stepi | step 走 C 语句,stepi 走机器指令 |
| 忘记查看参数寄存器 | 函数入口先看 %rdi/%rsi/%rdx/%rcx/%r8/%r9 |
| 把浮点参数当成整数参数 | System V AMD64 下优先查看 %xmm0-%xmm7 |
混淆 ss/sd 与 ps/pd | 前者是标量单/双精度,后者是打包单/双精度 |
浮点相等只检查 ZF | NaN 会产生无序结果,还要排除 PF = 1 |
把 cvtt 当作普通舍入 | cvtt 表示向零截断 |
15. 本章实验关联
| 内容 | 对应练习/实验 |
|---|---|
| 汇编阅读、条件跳转、循环 | exercises/chapter-03/*.s |
| 反汇编与控制流恢复 | labs/bomb-lab |
| 栈、过程调用、缓冲区溢出 | labs/attack-lab |
| SSE/AVX 浮点指令、参数与比较 | exercises/chapter-03/*.s |
| GDB 调试机器代码 | bomb-lab、attack-lab |