从汇编入手C语言
课程简介
课程地址:【结合汇编3小时彻底理解C语言本质】 https://www.bilibili.com/video/BV1NhM4zxEm7?vd_source=b235e9c478ba07e2678b1ac01bb439c6
结合汇编3小时彻底理解C语言本质,讲解C语言函数,语句,结构体,指针,数组,浮点数的汇编实现
C语言结构
用gcc -E class01.c -o class01.i
将源码预处理得到源程序(中间文件)
如图所示,实际上就是暴力展开 + 字符串替换
用gcc -S class01.i -o class01.s
将中间文件编译得到:
X86汇编结构:
利用 gcc -C class01.s -o class01.o
编译代码,此时代码已经不可阅读。
利用 objdump -S class01.o
查看代码
这样我们就可以看到每条汇编指令对应的机器指令
可以通过 readelf -a class01.o
查看每个段在程序中的偏移
当然,我是在windows环境下,所以使用 objdump
替代
1 | objdump -x class01.o # 查看头信息 |
x86汇编结构
变量定义以及其汇编细节
1. .data
段(已初始化的全局变量、静态变量)
存储:已初始化的非
const
全局变量或静态变量。示例:
1
2int a = 5; // 全局,存储在 .data
static int b = 3; // 静态,存储在 .data
2. .rodata
段(只读数据段)
存储:所有
const
修饰的全局变量和字符串字面量。示例:
1
2const int c = 10; // 存储在 .rodata
char *s = "hello"; // 字符串字面量存储在 .rodata
3. .bss
段(未初始化或初始化为 0 的全局变量)
存储:未初始化或初始化为 0 的全局变量、静态变量。
示例:
1
2
3int d; // 存储在 .bss
static int e; // 存储在 .bss
int f = 0; // 存储在 .bss
4. static
修饰的变量
特性:文件私有,作用域仅限当前源文件,链接属性为 local,无法通过
extern
访问。存储:
初始化:存储在
.data
或.bss
const static
:存储在.rodata
段名:不会有通用的
.local_var
段,正确描述是链接属性为 Local,段依然是.data
、.bss
、.rodata
。
5. 无修饰的全局变量
链接属性:全局可见,默认 external linkage,可被
extern
引用。存储:
已初始化:
.data
未初始化:
.bss
const
:.rodata
总结
变量类型 | 存储段 | 作用域 | 是否可被 extern 访问 |
---|---|---|---|
全局变量(已初始化) | .data |
全工程 | 可以 |
全局变量(未初始化/0 初始化) | .bss |
全工程 | 可以 |
const 全局变量 |
.rodata |
全工程 | 可以 |
static 全局变量 |
.data /.bss |
当前文件(文件私有) | 不可以 |
const static 变量 |
.rodata |
当前文件(文件私有) | 不可以 |
一般来说,已初始化的全局变量和静态变量分配在
.data
段;const
修饰的不可变全局变量和字符串字面量分配在.rodata
段(只读数据段);
未初始化的全局变量或初始化为 0 的变量分配在.bss
段,由操作系统在运行时自动清零。static
修饰的变量具有文件私有属性,它们不会出现在全局符号表中,即使使用extern
关键字也无法访问,存储位置仍可能在.data
、.bss
或.rodata
段,但链接属性为 local(本地符号),而不会存在所谓的.local_var
段。
未加修饰的全局变量具有全局可见性,可以被其他 C 文件通过extern
关键字访问。
变量命名规则
C语言函数的汇编实现
一般来说,函数中的普通局部变量存储在栈帧空间中,它们的生命周期随函数调用而动态变化;而定义的静态局部变量存储在全局段空间(如 .data
、.bss
或 .rodata
段),其生命周期贯穿整个程序运行过程,但作用域仅限于定义它的函数内部。
静态的函数和静态的变量一样,都具有私有保护,无法被导出使用。
C语言语句的汇编实现
1. 赋值(Assignment)
对应汇编:数据传送指令
核心指令:
mov
含义:将数据从一个寄存器/内存位置传送到另一个寄存器/内存位置
示例:
1
2mov eax, 5 ; 将 5 赋值给 eax
mov ebx, eax ; 将 eax 的值赋给 ebx总结表述:
赋值语句在汇编中通常对应数据传送指令,如mov
。
2. 函数调用(Function Call)
对应汇编:调用指令
核心指令:
call
含义:跳转到子程序,同时压栈当前指令地址(返回地址)
示例:
1
call func ; 调用 func 函数
总结表述:
函数调用在汇编中对应call
指令。
3. 函数返回(Return)
对应汇编:栈清理与返回指令
核心指令:
leave
+ret
含义:
leave
:恢复栈帧(等价于mov esp, ebp
+pop ebp
)ret
:从当前函数返回,跳转到压栈的返回地址
示例:
1
2leave
ret总结表述:
函数返回在汇编中通常由leave
和ret
实现。
4. 分支与循环(Branch & Loop)
对应汇编:比较与跳转指令
核心指令:
比较:
cmp
跳转:
je
(等于跳转)、jne
(不等跳转)、jg
、jl
、jmp
(无条件跳转)
示例:
1
2
3cmp eax, ebx ; 比较 eax 与 ebx
je label ; 如果相等,跳转到 label
jmp end ; 无条件跳转到 end分支结构(如 if-else)和循环结构(如 while、for)都是依赖
cmp
和jump
的组合。总结表述:
分支和循环控制在汇编中通常通过cmp
(比较指令)与各种条件跳转(如je
、jne
、jmp
)组合实现。
全面总结
C 语言中的基本语句与汇编指令的对应关系大致如下:
赋值操作: 对应
mov
等数据传送指令。函数调用: 对应
call
指令。函数返回: 通常由
leave
和ret
指令完成。分支与循环: 依赖
cmp
比较指令和条件跳转(如je
、jne
、jmp
)组合实现。
C运算符优先级
printf()详解
define预处理
整型实现
整型详解
整数加减乘除的汇编实现
虽然 有 add 和 sub 之分,但是内部都是基于 加法器实现的。
类型转换
浮点数实现
浮点数舍入原则:就近舍入
浮点数运算
但是这样会出现大数吃小数的现象,即小数在对阶的时候丢失精度了!