目录
前言
1. 编译的流程
2. 预编译
3. 编译
4. 汇编
5. 链接
前言
对于一个C程序,通常的开发环境都是流行的集成开发环境(IDE) ,例如Visual Studio、devc++等。通常在IDE内部集成了编译器和链接器,以至于我们不需要关注编译和链接的细节,只需要注意编程语言的语法正确性就可以写成能成功运行的代码,这种依赖于IDE自行完成的构建过程往往让我们忽略了程序运行的机制与机理,有时甚至产生一些我们无法理解的错误,因此了解程序的编译过程,能够帮助我们分析程序运行的本质,进一步地理解计算机系统
1. 编译的流程
对于大部分程序员的第一个C程序
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
即使是这样一个简单的C语言源程序,都需要经过 预编译 - 编译 - 汇编 - 链接 这4个步骤才能成为一个可执行的程序,如下图所示
2. 预编译
在预编译的阶段,C源代码文件 hello.c 和相关头文件被预处理器 (cpp) 处理成一个ASCII码的中间文件 hello.i ,使用如下命令进行预编译(-E选项表示只进行预编译)
gcc -E hello.c -o hello.i
预编译的过程主要处理源代码文件中使用“#”的预编译指令,包括头文件的包含以及宏定义等,主要规则如下
- 删除所有的 "#define "并展开所有的宏定义
- 处理所有的条件预编译指令,如"#if" "#ifdef"等
- 处理 "#include" 预编译指令,将被包含的文件插入到该预编译指令的位置
- 删除所有的注释 "//" 和 "/* */"
- 添加行号和文件名标识
- 保留所有的 #pragma 编译器指令,因为后面的编译过程会用到
考虑下面的代码
#include <stdio.h>
#define ADD +
int main()
{
//计算 3+5
int a = 3 ADD 5;
printf("%d\n", a);
return 0;
}
这是一个.c的c源代码文件,经过预编译后的.i文件为
可以看到以下特征:
- main函数从第10704行开始,前面的一大段代码是 <stdio.h> 头文件的插入
- 不包含任何宏定义,#define定义的宏被展开
- 不包含任何注释
3. 编译
在编译的阶段,预编译后的 hello.i 文件经编译器 (gcc) 进行一系列的词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件 hello.s ,使用如下命令进行预编译
gcc -S hello.i -o hello.s
事实上,编译器所做的事就是将高级语言翻译成了机器语言。编译器进行编译的过程是十分晦涩难懂的,但这个过程也是一个程序构建的核心部分,以后会对此进行详细的讨论
4. 汇编
在汇编的阶段,编译后的 hello.s 文件经汇编器 (as) 将其中的每条汇编代码转变成机器可执行的二进制指令并生成一个可重定位目标文件 (relocatable object files) hello.o,使用如下命令进行汇编
gcc -c hello.s -o hello.o
在这一过程中,汇编器仅仅是按照汇编指令和机器指令的对照表将汇编代码进行翻译,因此是比较简单的,但需要我们关注的是经过汇编后输出的.o文件,被称为可重定位目标文件,这种文件类型是链接过程的直接输入,因此十分重要
5. 链接
关于链接的部分,可以参考笔者的以下文章