C语言编译与链接(基础)¶
约 1799 个字 70 行代码 7 张图片 预计阅读时间 7 分钟
在ANSIC的任何一种实现中,存在两个不同的环境
- 翻译环境:源代码被转换为可执行的机器指令
- 运行环境:实际执行代码
翻译环境¶
翻译环境是由编译和链接两个过程组成的,而其中的编译过程可以分解成:预处理(也称预编译)、编译、汇编三个过程
当一个C语言项目(工程)中包含多个.c
为后缀的源文件时,将执行下面的步骤生成可执行程序
- 多个
.c
文件单独经过编译器编译处理生成对应的目标文件(目标文件也是二进制文件)
Note
在Windows环境下的目标文件的后缀是 .obj
,Linux环境下目标文件的后缀是 .o
- 多个目标文件和链接库⼀起经过链接器处理生成最终的可执行程序。
Note
链接库是指运行时库(它是支持程序运行的基本函数集合)或者第三方库
编译过程¶
预处理过程¶
在预处理阶段,源文件和头文件会被处理成为.i
为后缀的文件,预处理阶段主要处理那些源文件中#
开始的预编译指令
Text Only | |
---|---|
1 2 |
|
预处理过程处理规则:
- 将所有的
#define
删除,并展开所有的宏定义 - 处理
#include
预编译指令,将包含的头文件的内容插入到该预编译指令的位置。这个过程是递归进行的,即被包含的头文件也可能包含其他文件
C | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
- 处理所有的条件编译指令,如:
#if
、#ifdef
、#elif
、#else
、#endif
- 删除所有的注释(本质是将注释替换为空格)
- 添加行号和文件名标识,方便后续编译器生成调试信息等
- 保留所有的
#pragma
的编译器指令,编译器后续会使用
经过预处理后的.i
文件中不再包含宏定义,因为宏已经被展开。并且包含的头文件都被插入到.i
文件中。当需要查看宏定义或者头文件是否包含正确的时候,可以查看预处理后的.i
文件来确认
编译过程¶
编译过程就是将预处理后的⽂件进行一系列的:词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件以及符号汇总
Text Only | |
---|---|
1 2 |
|
Note
编译过程本质:将C语言代码翻译成汇编代码
词法分析¶
将源代码程序被输入扫描器,扫描器的任务就是简单的进行词法分析,把代码中的字符分割成一系列的记号(关键字、标识符、字面量、特殊字符等)
C | |
---|---|
1 2 |
|
进行词法分析后得到:
记号 | 类型 |
---|---|
array | 标识符 |
[ | 左方括号 |
index | 标识符 |
] | 右方括号 |
= | 赋值 |
( | 左圆括号 |
index | 标识符 |
+ | 加号 |
4 | 数值 |
) | 右圆括号 |
* | 乘号 |
( | 左圆括号 |
2 | 数值 |
+ | 加号 |
6 | 数值 |
) | 有圆括号 |
语法分析¶
在语法分析过程中,语法分析器将对扫描产生的记号进行语法分析,产生语法树。这些语法树是以表达式为节点的树
语义分析¶
由语义分析器来完成语义分析,即对表达式的语法层面分析。编译器所能做的分析是语义的静态分析。静态语义分析通常包括声明和类型的匹配,类型的转换等。这个阶段会报告错误的语法信息
生成符号汇总¶
对文件中的每个函数生成对应的符号
C | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
对于上面的实例代码中,在编译过程中将生成两张符号汇总表
对于add.c
的符号汇总表
函数 | 地址 |
---|---|
add(函数名) | 0x1000(函数有效地址) |
对于test.c
的符号汇总表
函数名 | 地址 |
---|---|
add(由声明外部符号引出的函数名) | 0x0000(无效地址) |
main(函数名) | 0x2000(函数有效地址) |
汇编¶
在汇编过程中,汇编器是将汇编代码转转变成机器可执行的指令,每一个汇编语句基本上都对应一条机器指令。就是根据汇编指令和机器指令的对照表一一的进行翻译,也不做指令优化,并且此过程会将在编译过程中生成的符号汇总转化成符号表
Text Only | |
---|---|
1 2 |
|
C | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
对于上面的实例代码中,在汇编过程中将两张符号汇总表转化成符号表
对于add.c
的符号汇总表
函数 | 地址 |
---|---|
add(函数名) | 0x1000(函数有效地址) |
对于test.c
的符号汇总表
函数 | 地址 |
---|---|
add(由声明外部符号引出的函数名) | 0x0000(无效地址) |
main(函数名) | 0x2000(函数有效地址) |
链接过程¶
链接过程主要包括:地址和空间分配,符号决议和重定位等过程
链接解决的是⼀个项目(工程)中多文件、多模块之间互相调用的问题
C | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
因为每个源文件都是单独经过编译器处理生成对应的目标文件,即test.c
文件经过编译器处理生成test.o
,add.c
文件经过编译器处理生成add.o
文件
重定位:在 test.c
文件中每一次使用 add
函数的时候必须确切的知道 add
的地址,但是由于每个⽂件是单独编译的,在编译器编译 test.c
的时候并不知道 add
函数的地址,所以暂时把调用 add
的指令的目标地址搁置(即符号表中的无效地址)。等待最后链接的时候由链接器根据引用的符号 add
在其他模块中查找 add
函数的地址,然后将 test.c
中所有引用到add 的指令重新修正,让他们的目标地址为实际存在的 add
函数的地址
Note
如果在连接过程中找不到对应的函数地址,即无效地址无法由有效地址替代时,将会报出“未定义的外部符号”类似的错误
运行环境¶
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序的执行便开始。接着便调用
main
函数。 - 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack)(也称为函数栈帧),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程⼀直保留他们的值。
- 终止程序。正常终止
main
函数或者意外终止main
函数