第04节-x86-64汇编入门
第 4 节:x86-64 汇编入门
所属课程:操作系统自学路线:面向网络安全、逆向工程与漏洞分析
所属周次:第 2 周
课程主题:C 语言、栈和汇编基础
本节目标:掌握 x86-64 汇编中最常见的寄存器、指令和调用约定,能初步读懂简单 C 程序对应的反汇编代码。
1. 本节课你要学会什么
学完这一节,你应该能回答下面几个问题:
- 汇编语言是什么?它和机器码、C 语言有什么关系?
- x86-64 中常见寄存器有哪些?
rax、rsp、rbp、rip分别有什么作用?mov、add、sub、cmp、jmp、call、ret大致是什么意思?- 汇编中的
[]表示什么? - Linux x64 和 Windows x64 函数传参有什么区别?
- 如何用
objdump和 GDB 看一个程序的反汇编? - 为什么逆向工程和漏洞分析离不开汇编?
本节课的主线是:
1 | |
你不需要一节课就精通汇编,但要开始能看懂最常见的指令和寄存器。
2. 汇编语言是什么
CPU 真正执行的是机器码。
机器码是二进制数据,例如:
1 | |
这些字节对人类来说很难直接阅读。
汇编语言是机器指令的文本表示。
例如上面的机器码可能对应:
1 | |
所以可以这样理解:
1 | |
大致关系:
1 | |
逆向工程中的“反汇编”则是反过来:
1 | |
常见反汇编工具:
- objdump
- GDB
- IDA
- Ghidra
- x64dbg
- WinDbg
3. 为什么网络安全要学汇编
如果你只写普通应用程序,可能很少直接写汇编。
但网络安全中,汇编非常重要。
3.1 逆向工程需要汇编
很多时候你拿不到源码,只能看到二进制程序。
工具能给你:
- 反汇编
- 反编译伪代码
- 字符串
- 调用关系
但最底层可靠的信息仍然是汇编。
如果反编译结果看不懂或不准确,就要回到汇编。
3.2 漏洞分析需要汇编
漏洞分析时你经常要看:
- 崩溃地址
- 当前执行到哪条指令
- 哪个寄存器异常
- 返回地址是否被覆盖
- 栈上数据如何变化
- 函数参数如何传入
这些都需要汇编基础。
3.3 恶意代码分析需要汇编
恶意代码经常会:
- 动态解析 API
- 加密字符串
- 使用反调试技巧
- 修改内存权限
- 跳转到动态生成代码
- 混淆控制流
这些行为经常需要看汇编才能确认。
4. x86、x64、x86-64 是什么
常见说法:
| 名称 | 含义 |
|---|---|
| x86 | 通常指 32 位 Intel 架构 |
| x64 | 通常指 64 位扩展架构 |
| x86-64 | 64 位 x86 架构的正式说法之一 |
| AMD64 | x86-64 的另一种常见名称 |
本课程主要学习:
1 | |
也就是现代 PC 上最常见的 64 位架构之一。
Linux、Windows 下的 64 位程序大多都运行在这个架构上。
5. 寄存器是什么
寄存器是 CPU 内部非常小但非常快的存储位置。
可以先理解为:
CPU 自己随身携带的一组变量。
程序执行时,CPU 经常把数据放到寄存器里进行计算。
例如:
1 | |
意思大致是:
1 | |
此时 eax 里就是 3。
内存容量大但慢,寄存器容量小但快。
所以 CPU 会频繁在寄存器和内存之间移动数据。
6. x86-64 常见通用寄存器
x86-64 中常见通用寄存器包括:
1 | |
它们都是 64 位寄存器。
其中最常见的几个:
| 寄存器 | 常见用途 |
|---|---|
| rax | 返回值、算术结果 |
| rbx | 通用寄存器,部分调用约定下需保持 |
| rcx | 通用寄存器,Windows x64 第 1 参数 |
| rdx | 通用寄存器,常用于参数 |
| rsi | Linux x64 第 2 参数,源地址相关操作 |
| rdi | Linux x64 第 1 参数,目标地址相关操作 |
| rsp | 栈顶指针 |
| rbp | 栈帧基址 |
| rip | 指令指针,表示下一条执行指令地址 |
| r8/r9 | 额外参数寄存器 |
注意:
rip 通常不归类为通用寄存器,但它极其重要。
7. 64 位、32 位、16 位、8 位寄存器名
同一个物理寄存器可以用不同宽度访问。
以 rax 为例:
| 名称 | 大小 |
|---|---|
| rax | 64 位 |
| eax | 低 32 位 |
| ax | 低 16 位 |
| al | 低 8 位 |
例如:
1 | |
此时:
1 | |
其他寄存器也有类似命名:
| 64 位 | 32 位 | 16 位 | 8 位 |
|---|---|---|---|
| rbx | ebx | bx | bl |
| rcx | ecx | cx | cl |
| rdx | edx | dx | dl |
| rsi | esi | si | sil |
| rdi | edi | di | dil |
| rbp | ebp | bp | bpl |
| rsp | esp | sp | spl |
一个重要细节:
在 x86-64 中,写入 32 位寄存器通常会把对应 64 位寄存器的高 32 位清零。
例如:
1 | |
执行后:
1 | |
8. rip:指令指针
rip 是非常重要的寄存器。
它保存:
1 | |
如果 rip 是:
1 | |
说明 CPU 当前要从地址 0x401136 处取指令执行。
程序的正常执行、跳转、函数调用、返回,本质上都在改变 rip。
例如:
- 顺序执行:
rip自动指向下一条指令 jmp:直接修改ripcall:保存返回地址后修改ripret:从栈中取返回地址放入rip
漏洞分析中,如果你看到:
1 | |
通常说明程序控制流已经被异常数据影响。
9. rsp 和 rbp:栈相关寄存器
上一节讲过:
rsp通常指向栈顶rbp常作为栈帧基址
常见函数开头:
1 | |
常见函数结尾:
1 | |
在调试器里,你需要经常观察:
1 | |
这三个寄存器可以帮助你判断:
- 当前执行到哪里
- 当前栈顶在哪里
- 当前栈帧在哪里
- 函数返回时可能跳到哪里
10. 汇编语法:Intel 和 AT&T
x86 汇编常见两种语法风格:
1 | |
本课程主要使用 Intel 语法。
Intel 语法常见形式:
1 | |
AT&T 语法常见形式:
1 | |
两者区别之一是操作数顺序不同。
Intel:
1 | |
例如:
1 | |
意思是:
1 | |
如果你用 objdump,默认可能看到 AT&T 语法。
可以加参数显示 Intel 语法:
1 | |
11. mov:数据移动
mov 是最常见的指令之一。
它的作用是:
1 | |
Intel 语法:
1 | |
例子:
1 | |
意思是:
1 | |
例子:
1 | |
意思是:
1 | |
例子:
1 | |
意思是:
1 | |
注意:
mov 不是“移动后源数据消失”,而是复制。
12. []:访问内存
在 Intel 汇编中,方括号 [] 表示访问内存。
例如:
1 | |
意思是:
1 | |
而:
1 | |
意思是:
1 | |
区别很大:
| 指令 | 含义 |
|---|---|
mov eax, rbp |
复制 rbp 的数值 |
mov eax, [rbp] |
把 rbp 当地址,读取该地址处的内容 |
mov eax, [rbp-4] |
读取 rbp-4 地址处的内容 |
这类似 C 语言中的:
1 | |
所以:
1 | |
类似:
1 | |
13. add 和 sub:加减
add 表示加法。
1 | |
意思是:
1 | |
sub 表示减法。
1 | |
意思是:
1 | |
在函数开头经常看到:
1 | |
这通常表示:
1 | |
因为 x86-64 中栈通常向低地址增长,所以 rsp 减小表示栈空间扩大。
14. push 和 pop:压栈和出栈
push 把数据压入栈。
1 | |
大致做:
1 | |
pop 从栈中取出数据。
1 | |
大致做:
1 | |
64 位模式下一次 push / pop 通常处理 8 字节。
函数开头常见:
1 | |
用于保存调用者的 rbp。
函数结尾常通过 leave 或 pop rbp 恢复。
15. cmp 和条件跳转
cmp 用于比较。
例如:
1 | |
它大致比较:
1 | |
但 cmp 本身不保存结果到普通寄存器,而是影响 CPU 的标志位。
后面通常跟条件跳转。
常见条件跳转:
| 指令 | 含义 |
|---|---|
| je | equal,相等则跳转 |
| jne | not equal,不相等则跳转 |
| jg | greater,大于则跳转 |
| jl | less,小于则跳转 |
| jge | greater or equal,大于等于则跳转 |
| jle | less or equal,小于等于则跳转 |
| ja | unsigned above,无符号大于 |
| jb | unsigned below,无符号小于 |
例子:
1 | |
意思是:
1 | |
16. jmp:无条件跳转
jmp 是无条件跳转。
例如:
1 | |
意思是:
1 | |
C 语言中的 if、for、while、switch 编译后,通常都会变成比较和跳转。
例如:
1 | |
可能变成:
1 | |
17. call 和 ret:函数调用和返回
上一节已经讲过。
call 大致做:
1 | |
ret 大致做:
1 | |
例如:
1 | |
逆向中看到 call 时,要问:
- 调用了哪个函数?
- 参数在哪里?
- 返回值是否被使用?
- 调用后程序如何继续?
漏洞分析中看到 ret 时,要问:
- 栈顶的返回地址是什么?
- 返回地址是否被覆盖?
rip会跳到哪里?
18. lea:取有效地址
lea 是 Load Effective Address。
它经常让初学者困惑。
例如:
1 | |
意思不是读取 [rbp-0x10] 里面的内容。
而是计算地址:
1 | |
也就是说,lea 取的是地址本身。
它类似 C 语言中的:
1 | |
例子:
1 | |
常见于把字符串地址传给 puts。
19. Linux x64 调用约定回顾
Linux x86-64 常用 System V AMD64 ABI。
整数和指针参数通常这样传:
| 参数序号 | 寄存器 |
|---|---|
| 第 1 个 | rdi |
| 第 2 个 | rsi |
| 第 3 个 | rdx |
| 第 4 个 | rcx |
| 第 5 个 | r8 |
| 第 6 个 | r9 |
返回值通常在:
1 | |
例如 C 代码:
1 | |
可能看到:
1 | |
表示:
1 | |
20. Windows x64 调用约定回顾
Windows x64 前四个整数或指针参数通常这样传:
| 参数序号 | 寄存器 |
|---|---|
| 第 1 个 | rcx |
| 第 2 个 | rdx |
| 第 3 个 | r8 |
| 第 4 个 | r9 |
返回值通常也在:
1 | |
所以同样的函数调用在 Windows 下可能是:
1 | |
表示:
1 | |
Windows x64 还有 shadow space 等规则,入门阶段只需要先知道:
1 | |
这对逆向非常关键。
21. C 代码如何对应汇编:加法函数
写一个程序 add_demo.c:
1 | |
编译:
1 | |
反汇编:
1 | |
你可能看到 add 类似:
1 | |
大致解释:
1 | |
你看到的真实汇编可能略有不同,但核心思想类似。
22. C 代码如何对应汇编:if 判断
写程序 if_demo.c:
1 | |
编译:
1 | |
反汇编:
1 | |
你会看到类似:
1 | |
对应关系:
| C 语言 | 汇编 |
|---|---|
if (x == 10) |
cmp + 条件跳转 |
| then 分支 | 一段顺序代码 |
| else 分支 | 另一段顺序代码 |
| 分支结束 | 跳到共同出口 |
逆向时,看到 cmp 和 jcc,通常要想到:
1 | |
23. C 代码如何对应汇编:循环
写程序 loop_demo.c:
1 | |
编译:
1 | |
反汇编后,你通常会看到:
- 初始化变量
- 比较条件
- 条件跳转
- 循环体
- 自增
- 跳回比较处
循环的底层本质就是:
1 | |
所以汇编里并没有真正的 for 或 while。
它们都会变成跳转结构。
24. GDB 中查看汇编
用 GDB 调试 add_demo:
1 | |
常用命令:
1 | |
解释:
| 命令 | 用途 |
|---|---|
disassemble main |
反汇编 main 函数 |
layout asm |
显示汇编窗口 |
info registers |
查看寄存器 |
stepi |
单步执行一条机器指令,进入函数 |
nexti |
单步执行一条机器指令,不进入函数 |
如果 layout asm 显示不正常,可以直接用:
1 | |
x/10i $rip 表示:
1 | |
25. objdump 和 GDB 的区别
objdump 用于静态查看文件中的反汇编。
例如:
1 | |
它不运行程序,只分析文件。
GDB 是动态调试器。
它可以:
- 运行程序
- 下断点
- 单步执行
- 查看寄存器
- 查看栈
- 查看内存
- 修改变量或寄存器
所以:
1 | |
逆向和漏洞分析中,两者经常配合使用。
26. Windows 对照:x64dbg 中看汇编
Windows 下用 x64dbg 打开 exe 后,默认主窗口就是汇编视图。
常见区域:
- 左上:反汇编代码
- 右上:寄存器
- 左下:内存 Dump
- 右下:栈
你可以观察:
- 当前
RIP指向哪条指令 RCX/RDX/R8/R9中是否有参数RAX中是否有返回值RSP指向的栈内容call调用了哪个函数ret返回到哪里
如果你调试一个调用 MessageBoxA 的程序,可以观察:
1 | |
这些寄存器中如何传入参数。
27. 汇编阅读的基本方法
初学者读汇编不要一行一行死抠。
建议按层次阅读。
27.1 先找函数边界
看:
- 函数入口
- 函数结尾
callret
先知道哪里是一个函数。
27.2 再找关键调用
重点看调用了什么函数:
printfputsmallocfreeopenreadwriteCreateFileWVirtualAllocCreateProcessW
函数调用常常比普通算术指令更有语义价值。
27.3 再看参数
根据调用约定,看调用前寄存器里放了什么。
Linux:
1 | |
Windows:
1 | |
27.4 再看分支
看 cmp 和跳转:
jejnejgjljmp
这些通常对应条件判断和循环。
27.5 最后看局部变量
看 [rbp-...] 或 [rsp+...]。
它们通常是:
- 局部变量
- 临时数据
- 保存的参数
- 栈上的缓冲区
28. 从逆向工程角度看本节内容
汇编是逆向工程的基础语言。
你需要逐渐建立这些直觉:
1 | |
可能是:
1 | |
1 | |
可能是:
1 | |
1 | |
可能是:
1 | |
逆向不是把每条汇编翻译成 C,而是恢复程序意图。
29. 从漏洞分析角度看本节内容
漏洞分析中,你常常要看崩溃现场。
例如:
1 | |
然后查看当前指令:
1 | |
如果 rax = 0,这就可能是空指针解引用。
再例如:
1 | |
执行前栈顶是:
1 | |
那么 ret 后 rip 可能变成这个值,导致崩溃。
所以汇编能帮助你判断:
- 程序为什么崩溃
- 哪个地址非法
- 哪个寄存器异常
- 输入如何影响控制流
- 防护机制是否生效
30. 从恶意代码分析角度看本节内容
恶意代码经常故意隐藏高级语义。
反编译结果可能很乱,但汇编仍然可靠。
你需要关注:
- 是否调用敏感 API
- 参数指向哪里
- 是否有异常跳转
- 是否动态计算函数地址
- 是否修改内存权限
- 是否写入代码后跳转执行
例如看到:
1 | |
可能意味着:
1 | |
这在恶意代码分析中非常值得关注。
31. 本节 Linux 实验:反汇编 add 程序
创建 add_demo.c:
1 | |
编译:
1 | |
反汇编:
1 | |
要求你找到:
mainaddcall addcall printfret- 参数传递相关的
edi、esi - 返回值相关的
eax
32. 本节 Linux 实验:GDB 单步执行汇编
启动:
1 | |
执行:
1 | |
观察:
- 每执行一条指令,
rip如何变化 - 调用
add前,edi和esi是什么 add返回后,eax是什么- 调用
printf前,参数寄存器是什么
33. 本节重点总结
你需要记住这些核心结论:
- 汇编是机器码的人类可读形式。
- 反汇编是把机器码转换成汇编表示。
- 寄存器是 CPU 内部高速存储位置。
rip表示下一条要执行的指令地址。rsp表示栈顶,rbp常用作栈帧基址。mov用于复制数据。[]表示访问内存。add/sub用于加减。cmp通常配合条件跳转实现判断。jmp是无条件跳转。call和ret实现函数调用和返回。- Linux x64 和 Windows x64 的参数寄存器不同。
- 逆向、漏洞分析和恶意代码分析都离不开汇编基础。
34. 本节课后作业
作业 1:反汇编 add 程序
完成 add_demo.c,执行:
1 | |
提交内容:
1 | |
作业 2:分析 if 程序
写 if_demo.c:
1 | |
反汇编后找到:
cmp- 条件跳转指令
- then 分支
- else 分支
回答:
1 | |
作业 3:GDB 单步汇编
用 GDB 执行:
1 | |
至少单步执行 10 条指令。
提交内容:
1 | |
35. 自测题
题 1
机器码和汇编语言有什么关系?
题 2
rax、rsp、rbp、rip 分别常见用途是什么?
题 3
Intel 语法中 mov eax, 1 是什么意思?
题 4
mov eax, [rbp-4] 和 mov eax, rbp 有什么区别?
题 5
cmp 指令通常和什么指令一起使用?
题 6
Linux x64 前 6 个整数或指针参数通常放在哪些寄存器?
题 7
Windows x64 前 4 个整数或指针参数通常放在哪些寄存器?
题 8
为什么汇编对逆向工程很重要?
36. 自测题参考答案
答 1
机器码是 CPU 实际执行的二进制字节;汇编语言是机器码的人类可读文本表示。反汇编就是把机器码转换成汇编表示。
答 2
rax 常用于返回值和计算结果;rsp 指向栈顶;rbp 常作为栈帧基址;rip 表示下一条要执行的指令地址。
答 3
把立即数 1 复制到 eax 寄存器中。
答 4
mov eax, rbp 是把 rbp 寄存器的值复制到 eax;mov eax, [rbp-4] 是把 rbp-4 当作地址,读取该地址处的内存内容到 eax。
答 5
cmp 通常和条件跳转指令一起使用,例如 je、jne、jg、jl 等。
答 6
Linux x64 System V ABI 中,前 6 个整数或指针参数通常依次放在 rdi、rsi、rdx、rcx、r8、r9。
答 7
Windows x64 中,前 4 个整数或指针参数通常依次放在 rcx、rdx、r8、r9。
答 8
因为逆向时经常没有源码,只能通过反汇编理解程序。函数调用、条件判断、循环、API 参数、崩溃现场和控制流都需要通过汇编分析。
37. 下一节预告
下一节课会讲:
1 | |
你会学习:
- 调试器为什么能让程序暂停
- 断点是什么
- 单步执行是什么
- 寄存器窗口和内存窗口怎么看
- Linux 的
ptrace - Windows Debug API
- 恶意代码为什么会反调试