- Hello World
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
打印helllo world相信每个人都不陌生,那么在编译hello world 的时候在集成开发环境中执行了几个步骤呢?
上述过程可以分解为 4 个步骤,分别是 预处理(Prepressing)、编译(Compilaton)、汇编(Assembly) 和 链接(Linking)
如图所示
预编译
- 预编译处理
预编译过程主要处理那些源代的文件中的以 # 开始的预编译指令。比如 #include、#define 等,主要处理规则如下:
将所有的 #define 删除,并且展开所有的宏定义。
1.1 处理所有条件编译指令,比如:#if、#ifdef、#elif、#else、#endif。
1.2 处理 #include 预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
1.3 删除所有的注释 // 和 /* */。
1.4 添加行号和文件名标识,比如 #2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
1.5 保留所有的 #pragma 编译器指令,因为编译器须要使用它们。
编译
- 编译过程就是将预处理后的⽂件进⾏⼀系列的:词法分析、语法分析、语义分析及优化,⽣成相应的
汇编代码⽂件。
2.1 词法分析
首先源代码程序被输入到 扫描器,扫描器的任务很简单,它只是简单地进行词法分析,运用一种类似于 有限状态机的算法可以很轻松地将源代码的字符序列分割成一系列的 记号。
词法分析产生的记号一般可以分为如下几类:关键字、标识符、字面量(包含数字、字符串等) 和 特殊符号(如加号、等号)。在识别记号的同时,扫描器也完成了其他工作。比如将标识符存放到 符号表,将数字、字符串常量存放到 文字表 等,以备后面的步骤使用。
2.2 语法分析
由语法分析器生成的语法树就是以 表达式为节点的树。我们知道,C 语言的一个语句是一个表达式, 而复杂的语句是很多表达式的组合。上面例子中的语句就是一个由赋值表达式、加法表达式、乘法表达式、数组表达式、括号表达式组成的复杂语句。它在经过语法分析器以后形成语法树。
2.3 语义分析
接下来进行的是语义分析,由语义分析器来完成。语法分析仅仅是完成了对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意义。比如 C 语言里面两个指针做乘法运算是没有意义的,但是这个语句在语法上是合法的;比如同样一个指针和一个浮点数做乘法运算是否合法等。编译器所能分析的语义是静态语义,所谓静态语义是指在编译期可以确定的语义,与之对应的动态语义就是只有在运行期才能确定的语义。
2.4 目标代码生成与优化
源代码级优化器产生中间代码标志着下面的过程都属于编译器后端。编译器后端主要包括代码生成器和目标代码优化器。让我们先来看看代码生成器。代码生成器将中间代码转换成目标机器代码,这个过程十分依赖于目标机器,因为不同的机器有着不同的字长、寄存器、整数数据类型和浮点数数据类型等。
汇编
- 汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。
所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇篇指令和机器指令的对照表一一翻译就可以了,“汇编”这个名字也来源于此。上而的汇编过程我们可以调用汇编器 as 来完成:
链接
- 链接是⼀个复杂的过程,链接的时候需要把⼀堆⽂件链接在⼀起才⽣成可执⾏程序。
链接过程主要包括:地址和空间分配,符号决议和重定位等这些步骤。
链接解决的是⼀个项⽬中多⽂件、多模块之间互相调⽤的问题
test.c
#include <stdio.h>
//test.c
//声明外部函数
extern int Add(int x, int y);
//声明外部的全局变量
extern int g_val;
int main()
{
int a = 10;
int b = 20;
int sum = Add(a, b);
printf("%d\n", sum);
return 0;
}
add.c
int g_val = 2022;
int Add(int x, int y)
{
return x+y;
}
我们已经知道,每个源⽂件都是单独经过编译器处理⽣成对应的⽬标⽂件。
- test.c 经过编译器处理⽣成 test.o
add.c 经过编译器处理⽣成 add.o
我们在 test.c 的⽂件中使⽤了 add.c ⽂件中的 Add 函数和 g_val 变量。
我们在 test.c ⽂件中每⼀次使⽤ Add 函数和 g_val 的时候必须确切的知道 Add 和 g_val 的地
址,但是由于每个⽂件是单独编译的,在编译器编译 test.c 的时候并不知道 Add 函数和 g_val
变量的地址,所以暂时把调⽤ Add 的指令的⽬标地址和 g_val 的地址搁置。等待最后链接的时候由
链接器根据引⽤的符号 Add 在其他模块中查找 Add 函数的地址,然后将 test.c 中所有引⽤到
Add 的指令重新修正,让他们的⽬标地址为真正的 Add 函数的地址,对于全局变量 g_val 也是类
似的⽅法来修正地址。这个地址修正的过程也被叫做:重定位。
本篇介绍到此为止 如果想对编译和链接更加了解可以参考 程序员的自我修养里面详细介绍了编译与链接的底层逻辑