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:编写第一个汇编程序¶
目的:学习汇编程序的基本结构
步骤:
-
创建汇编文件
GAS# 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调用汇编函数
步骤:
-
创建汇编文件
-
创建C文件
-
编译链接
实验3:观察编译器生成的汇编¶
目的:理解C代码如何翻译成汇编
步骤:
-
创建C文件
-
生成汇编代码(不同优化级别)
-
对比分析
实验4:使用内联汇编¶
目的:在C代码中嵌入汇编
步骤:
-
创建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微架构、流水线、分支预测和缓存机制。