目录
一、C 语言与汇编的关系
C 代码通过编译器(如 GCC)转换为汇编指令,最终生成机器码。汇编是 C 和机器码之间的桥梁,直接操作寄存器、内存和 CPU 指令。
1. 查看 C 代码的汇编输出
使用 GCC 生成汇编代码:
gcc -S -O0 -masm=intel test.c # 生成 Intel 格式汇编(test.s)
-
-O0
:禁用优化,保留代码原始结构 -
-masm=intel
:指定 Intel 汇编语法(更易读)
二、汇编指令基础格式
Intel 格式汇编指令一般结构:
指令 [目标操作数], [源操作数] ; 注释
-
指令:操作类型(如
mov
,add
,call
) -
操作数:寄存器、内存地址、立即数
-
注释:用
;
开头,解释代码逻辑
常见寄存器(x86-64架构)
三、C 代码与汇编指令对照
示例 1:简单加法
C 代码:
int main() {
int a = 10;
int b = 20;
int c = a + b;
return c;
}
对应汇编(关键部分):
main:
push ebp ; 保存旧的基址指针
mov ebp, esp ; 设置新的栈帧基址
sub esp, 16 ; 为局部变量分配栈空间(16字节对齐)
mov DWORD PTR [ebp-4], 10 ; a = 10(存储到栈地址 ebp-4)
mov DWORD PTR [ebp-8], 20 ; b = 20(存储到栈地址 ebp-8)
mov eax, DWORD PTR [ebp-4] ; 将a的值加载到eax
add eax, DWORD PTR [ebp-8] ; eax = eax + b(即a + b)
mov DWORD PTR [ebp-12], eax ; c = eax(结果存到ebp-12)
mov eax, DWORD PTR [ebp-12] ; 返回值存入eax
leave ; 恢复栈帧(mov esp, ebp; pop ebp)
ret ; 函数返回
关键点:
-
局部变量存储在栈上:通过
ebp - offset
访问(如ebp-4
)。 -
运算通过寄存器中转:先将变量加载到
eax
,再执行add
操作。 -
返回值通过
eax
传递:函数结束时,eax
的值即返回值。
示例 2:函数调用
C 代码:
int add(int x, int y) {
return x + y;
}
int main() {
int result = add(3, 5);
return result;
}
对应汇编(关键部分):
add:
push ebp
mov ebp, esp
mov eax, DWORD PTR [ebp+8] ; 取第一个参数x(ebp+8)
add eax, DWORD PTR [ebp+12] ; 加第二个参数y(ebp+12)
pop ebp
ret
main:
push ebp
mov ebp, esp
sub esp, 16
push 5 ; 第二个参数压栈
push 3 ; 第一个参数压栈
call add ; 调用函数
add esp, 8 ; 清理栈上的参数(2个int,共8字节)
mov DWORD PTR [ebp-4], eax ; result = eax
mov eax, DWORD PTR [ebp-4] ; 返回值
leave
ret
关键点:
-
参数传递:通过栈传递参数(32位模式下),
ebp+8
是第一个参数,ebp+12
是第二个参数。 -
函数调用步骤:
-
参数从右向左压栈(先
push 5
,再push 3
)。 -
call
指令将返回地址压栈,并跳转到函数。 -
函数返回后,调用者负责清理栈上的参数(
add esp, 8
)。
-
四、内存访问与寻址模式
汇编通过不同寻址方式访问内存:
1. 直接寻址
mov eax, DWORD PTR [0x12345678] ; 从内存地址0x12345678加载数据到eax
2. 寄存器间接寻址
mov ebx, 0x12345678
mov eax, DWORD PTR [ebx] ; 通过ebx中的地址访问内存
3. 基址偏移寻址
mov eax, DWORD PTR [ebp-4] ; 访问栈上的局部变量(基址为ebp)
五、关键指令详解
六、实战:循环结构
C 代码:
int main() {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
return sum;
}
对应汇编(关键部分):
main:
push ebp
mov ebp, esp
sub esp, 16
mov DWORD PTR [ebp-4], 0 ; sum = 0
mov DWORD PTR [ebp-8], 0 ; i = 0
.L2:
cmp DWORD PTR [ebp-8], 10 ; 比较i和10
jge .L4 ; 如果i >= 10,跳转到.L4(退出循环)
mov eax, DWORD PTR [ebp-8] ; eax = i
add DWORD PTR [ebp-4], eax ; sum += eax
add DWORD PTR [ebp-8], 1 ; i++
jmp .L2 ; 跳转回.L2(继续循环)
.L4:
mov eax, DWORD PTR [ebp-4] ; 返回值sum
leave
ret
关键点:
-
循环控制:通过
cmp
和条件跳转指令(如jge
)实现。 -
标签(Label):
.L2
和.L4
是编译器生成的跳转标签。
七、如何深入学习?
-
查看编译后的汇编:
使用gcc -S
生成汇编代码,结合-O0
(无优化)和-O2
(优化)对比分析。 -
调试器分析:
用 GDB 单步执行程序,观察寄存器和内存变化。 -
参考手册:参考手册
http://Intel 64 and IA-32 Architectures Software Developer Manuals
-
实践练习:
尝试用汇编重写简单 C 函数(如计算阶乘)。