02-指令集与汇编语言¶
重要性:⭐⭐⭐⭐⭐ 实用度:⭐⭐⭐⭐ 学习时间: 3 天 必须掌握:是
为什么学这一章¶
汇编语言是人与机器之间的桥梁。学习汇编能帮助你: - 理解 C/C++代码的底层实现 - 调试复杂的程序崩溃问题 - 编写高性能的底层代码 - 理解编译器优化的原理
学完这一章,你将能够: - ✅ 阅读和理解 x86-64 汇编代码 - ✅ 编写简单的汇编程序 - ✅ 理解 C 代码与汇编的对应关系 - ✅ 使用汇编进行底层优化
📖 核心概念¶
1. 什么是汇编语言¶
┌─────────────────────────────────────────────────────────────────────┐
│ 从高级语言到机器码 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ C/C++代码 │
│ int add(int a, int b) { │
│ return a + b; │
│ } │
│ ↓ 编译器 │
│ 汇编代码 │
│ add: │
│ mov eax, edi ; 将第一个参数(edi)移到eax │
│ add eax, esi ; 加上第二个参数(esi) │
│ ret ; 返回(结果在eax中) │
│ ↓ 汇编器 │
│ 机器码(十六进制) │
│ 89 F8 01 F0 C3 │
│ ↓ CPU执行 │
│ 程序运行 │
│ │
└─────────────────────────────────────────────────────────────────────┘
汇编语言的特点: - 每条汇编指令对应一条或多条机器指令 - 直接操作寄存器和内存 - 与 CPU 架构紧密相关( x86, ARM, MIPS 等) - 可读性比机器码好,但比高级语言差
2. x86-64 汇编基础¶
基本语法格式¶
; AT&T语法(GCC默认)
movl $42, %eax ; 将立即数42移到eax
addl %ebx, %eax ; ebx加到eax
movl (%rdi), %eax ; 从rdi指向的内存读取到eax
; Intel语法(Windows/MASM)
mov eax, 42 ; 将42移到eax
add eax, ebx ; ebx加到eax
mov eax, [rdi] ; 从rdi指向的内存读取到eax
本教程使用 AT&T 语法( Linux/GCC 默认),特点: - 寄存器前加%(如%eax) - 立即数前加$(如$42) - 源操作数在前,目的操作数在后 - 内存寻址用()(如(%rdi))
常用指令分类¶
┌─────────────────────────────────────────────────────────────────────┐
│ x86-64常用指令分类 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 数据传送指令 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ mov src, dst ; dst = src │ │
│ │ push src ; 压栈:rsp -= 8; [rsp] = src │ │
│ │ pop dst ; 出栈:dst = [rsp]; rsp += 8 │ │
│ │ lea src, dst ; dst = &src(加载有效地址) │ │
│ │ xchg src, dst ; 交换src和dst │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 2. 算术运算指令 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ add src, dst ; dst += src │ │
│ │ sub src, dst ; dst -= src │ │
│ │ mul src ; rax *= src(无符号乘法) │ │
│ │ imul src ; rax *= src(有符号乘法) │ │
│ │ div src ; rax /= src(无符号除法) │ │
│ │ idiv src ; rax /= src(有符号除法) │ │
│ │ inc dst ; dst++ │ │
│ │ dec dst ; dst-- │ │
│ │ neg dst ; dst = -dst │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 3. 逻辑运算指令 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ and src, dst ; dst &= src │ │
│ │ or src, dst ; dst |= src │ │
│ │ xor src, dst ; dst ^= src │ │
│ │ not dst ; dst = ~dst │ │
│ │ shl count, dst ; dst <<= count(逻辑左移) │ │
│ │ shr count, dst ; dst >>= count(逻辑右移) │ │
│ │ sar count, dst ; dst >>= count(算术右移) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 4. 比较和跳转指令 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ cmp src, dst ; 比较dst和src,设置标志位 │ │
│ │ test src, dst ; dst & src,设置标志位 │ │
│ │ jmp label ; 无条件跳转到label │ │
│ │ je label ; 等于则跳转(ZF=1) │ │
│ │ jne label ; 不等于则跳转(ZF=0) │ │
│ │ jg label ; 大于则跳转(有符号) │ │
│ │ jl label ; 小于则跳转(有符号) │ │
│ │ ja label ; 大于则跳转(无符号) │ │
│ │ jb label ; 小于则跳转(无符号) │ │
│ │ call label ; 调用函数 │ │
│ │ ret ; 从函数返回 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
3. 操作数类型¶
┌─────────────────────────────────────────────────────────────────────┐
│ x86-64操作数类型 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 立即数(Immediate) │
│ 直接写在指令中的常数 │
│ mov $42, %eax ; 将42移到eax │
│ add $10, %ebx ; ebx加10 │
│ │
│ 2. 寄存器(Register) │
│ CPU内部的存储单元 │
│ mov %eax, %ebx ; 将eax的值复制到ebx │
│ add %ecx, %edx ; ecx加到edx │
│ │
│ 3. 内存(Memory) │
│ 通过地址访问的内存数据 │
│ │
│ 直接寻址: │
│ mov 0x1000, %eax ; 从地址0x1000读取4字节到eax │
│ │
│ 寄存器间接寻址: │
│ mov (%rbx), %eax ; 从rbx指向的地址读取到eax │
│ │
│ 基址+偏移寻址: │
│ mov 8(%rbx), %eax ; 从rbx+8的地址读取到eax │
│ │
│ 基址+索引+比例寻址: │
│ mov (%rbx, %rcx, 2), %eax ; 从rbx + rcx*2的地址读取 │
│ │
│ 复杂寻址示例: │
│ mov 16(%rbx, %rcx, 4), %eax ; 从rbx + rcx*4 + 16读取 │
│ │
└─────────────────────────────────────────────────────────────────────┘
4. C 代码与汇编对照¶
示例 1 :简单函数¶
# 汇编代码(AT&T语法)
add:
push %rbp # 保存旧的基址指针
mov %rsp, %rbp # 设置新的基址指针
mov %edi, -4(%rbp) # 保存参数a到栈
mov %esi, -8(%rbp) # 保存参数b到栈
mov -4(%rbp), %edx # 加载a到edx
mov -8(%rbp), %eax # 加载b到eax
add %edx, %eax # a + b,结果在eax
pop %rbp # 恢复基址指针
ret # 返回
简化版本(优化后):
示例 2 :条件判断¶
# 汇编代码
max:
mov %edi, %eax # eax = a
cmp %esi, %edi # 比较a和b
jg .L_return_a # 如果a > b,跳转到.L_return_a
mov %esi, %eax # 否则,eax = b
.L_return_a:
ret # 返回(结果在eax中)
示例 3 :循环¶
// C代码
int sum(int n) {
int result = 0;
for (int i = 1; i <= n; i++) {
result += i;
}
return result;
}
# 汇编代码
sum:
mov $0, %eax # result = 0
mov $1, %ecx # i = 1
.L_loop:
cmp %edi, %ecx # 比较i和n
jg .L_end # 如果i > n,结束循环
add %ecx, %eax # result += i
inc %ecx # i++
jmp .L_loop # 继续循环
.L_end:
ret # 返回result
5. 函数调用约定¶
System V AMD64 ABI ( Linux/macOS )¶
┌─────────────────────────────────────────────────────────────────────┐
│ x86-64函数调用约定 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 参数传递(前6个整数/指针参数): │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 第1参数:RDI │ │
│ │ 第2参数:RSI │ │
│ │ 第3参数:RDX │ │
│ │ 第4参数:RCX │ │
│ │ 第5参数:R8 │ │
│ │ 第6参数:R9 │ │
│ │ 第7+参数:通过栈传递 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 返回值: │
│ ├── 整数/指针:RAX │
│ ├── 128位整数:RAX(低64位)+ RDX(高64位) │
│ └── 浮点数:XMM0 │
│ │
│ 调用者保存的寄存器(Caller-saved): │
│ ├── RAX, RCX, RDX, RSI, RDI, R8-R11 │
│ └── 调用者需要在调用前保存这些寄存器 │
│ │
│ 被调用者保存的寄存器(Callee-saved): │
│ ├── RBX, RBP, R12-R15 │
│ └── 被调用函数需要保存并在返回前恢复 │
│ │
│ 栈对齐: │
│ └── 调用前RSP必须是16字节对齐 │
│ │
└─────────────────────────────────────────────────────────────────────┘
函数调用示例¶
// C代码
int callee(int a, int b, int c, int d, int e, int f, int g) {
return a + b + c + d + e + f + g;
}
int caller() {
return callee(1, 2, 3, 4, 5, 6, 7);
}
# 汇编代码
callee:
push %rbp
mov %rsp, %rbp
# 前6个参数在寄存器中
# a: %edi, b: %esi, c: %edx, d: %ecx, e: %r8d, f: %r9d
# 第7个参数在栈上:16(%rbp)
mov 16(%rbp), %eax # g
add %edi, %eax # + a
add %esi, %eax # + b
add %edx, %eax # + c
add %ecx, %eax # + d
add %r8d, %eax # + e
add %r9d, %eax # + f
pop %rbp
ret
caller:
push %rbp
mov %rsp, %rbp
sub $8, %rsp # 栈对齐(16字节对齐)
push $7 # 第7个参数压栈
mov $6, %r9d # 第6个参数
mov $5, %r8d # 第5个参数
mov $4, %ecx # 第4个参数
mov $3, %edx # 第3个参数
mov $2, %esi # 第2个参数
mov $1, %edi # 第1个参数
call callee
add $16, %rsp # 清理栈
pop %rbp
ret
🧪 动手实验¶
实验 1 :编写第一个汇编程序¶
目的:学习汇编程序的基本结构
步骤:
- 创建汇编文件
# hello.asm
.section .data
msg: .ascii "Hello, World!\n"
len = . - msg
.section .text
.globl _start
_start:
# write(1, msg, len)
mov $1, %rax # syscall: write
mov $1, %rdi # fd: stdout
mov $msg, %rsi # buf: msg
mov $len, %rdx # count: len
syscall
# exit(0)
mov $60, %rax # syscall: exit
mov $0, %rdi # status: 0
syscall
- 编译运行
实验 2 : C 与汇编混合编程¶
目的:学习 C 调用汇编函数
步骤:
- 创建汇编文件
# add.asm
.section .text
.globl asm_add
# int asm_add(int a, int b)
asm_add:
mov %edi, %eax # eax = a
add %esi, %eax # eax += b
ret
- 创建 C 文件
// main.c
#include <stdio.h>
// 声明外部汇编函数
extern int asm_add(int a, int b);
int main() {
int result = asm_add(10, 20);
printf("10 + 20 = %d\n", result);
return 0;
}
- 编译链接
实验 3 :观察编译器生成的汇编¶
目的:理解 C 代码如何翻译成汇编
步骤:
- 创建 C 文件
// test.c
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
int main() {
int result = factorial(5);
return result;
}
- 生成汇编代码(不同优化级别)
# 无优化
gcc -S -O0 test.c -o test_O0.s
# 中等优化
gcc -S -O2 test.c -o test_O2.s
# 最高优化
gcc -S -O3 test.c -o test_O3.s
- 对比分析
实验 4 :使用内联汇编¶
目的:在 C 代码中嵌入汇编
步骤:
- 创建 C 文件
// inline_asm.c
#include <stdio.h> // 引入头文件
int add_inline(int a, int b) {
int result;
__asm__ volatile (
"add %1, %0\n\t"
: "=r" (result) // 输出操作数
: "r" (a), "0" (b) // "0" 约束使 b 与 result 共用同一寄存器
: // 无破坏的寄存器
);
return result;
}
int main() {
int result = add_inline(10, 20);
printf("Result: %d\n", result);
return 0;
}
- 编译运行
💡 核心要点总结¶
常用指令速查¶
| 指令 | 功能 | 示例 |
|---|---|---|
| mov | 数据传送 | mov %eax, %ebx |
| push/pop | 栈操作 | push %rax / pop %rax |
| add/sub | 加减 | add %ebx, %eax |
| mul/imul | 乘法 | mul %ebx |
| div/idiv | 除法 | div %ebx |
| and/or/xor | 逻辑运算 | and %ebx, %eax |
| cmp | 比较 | cmp %ebx, %eax |
| jmp/je/jg | 跳转 | je label |
| call/ret | 函数调用/返回 | call func |
寄存器用途速查¶
| 寄存器 | 用途 |
|---|---|
| RAX | 返回值、累加器 |
| RBX | 被调用者保存 |
| RCX | 第 4 参数、计数器 |
| RDX | 第 3 参数、数据 |
| RSI | 第 2 参数、源索引 |
| RDI | 第 1 参数、目的索引 |
| RBP | 基址指针 |
| RSP | 栈指针 |
| R8-R9 | 第 5-6 参数 |
| R10-R11 | 调用者保存 |
| R12-R15 | 被调用者保存 |
C 与汇编对应关系¶
| C 结构 | 汇编实现 |
|---|---|
| 函数 | 标签 + ret |
| 参数 | 寄存器/栈传递 |
| 返回值 | RAX 寄存器 |
| 局部变量 | 栈空间 |
| if/else | cmp + 条件跳转 |
| for/while | 标签 + 条件跳转 |
| 函数调用 | call + 参数设置 |
❓ 常见问题¶
Q1 : AT&T 语法和 Intel 语法有什么区别?
A :主要区别: - AT&T :源在前,目的在后;寄存器加%,立即数加$ - Intel :目的在前,源在后;无特殊前缀 - Linux/GCC 默认 AT&T , Windows/MASM 使用 Intel
Q2 :为什么汇编代码中有那么多mov指令?
A : x86 架构是 CISC (复杂指令集),但现代 CPU 内部将复杂指令分解成简单操作。mov是最基本的操作,用于数据在寄存器和内存之间移动。
Q3 :如何学习汇编编程?
A :建议步骤: 1. 先学习阅读汇编(从 C 代码生成汇编) 2. 理解函数调用约定 3. 尝试修改汇编代码 4. 最后尝试独立编写
Q4 :汇编编程还有什么用?
A :现代应用: - 系统编程(操作系统、驱动) - 性能优化(关键路径) - 逆向工程和安全研究 - 嵌入式系统
📚 扩展阅读¶
- 《深入理解计算机系统》 - 第 3 章:程序的机器级表示
- 《 x86-64 汇编语言》 - 相关书籍
- Intel 手册: Volume 2 - Instruction Set Reference
- 在线资源: x86asm.net
🎯 下一步¶
继续学习 CPU 与指令执行的后续内容,深入了解 CPU 微架构、流水线、分支预测和缓存机制。