目录
一、引言
二、编译和链接
2.1 预处理(预编译)
2.2 编译
2.3 汇编
2.4 链接
一、引言
#include <stdio.h>
int main()
{printf("hello, world\n");return 0;
}
像这样,只由ASCII字符构成的文件被称为文本文件,它需要经过翻译环境的编译和链接后才能成为二进制文件,也就是可执行程序。
可执行程序的内部是一系列二进制形式的计算机指令和数据的集合,CPU可以直接识别,但是对于程序员来说非常难以记忆和使用,所以程序员开发出了编程语言,使用这些语言来编写程序。
为了要运行环境中运行编写出来的程序,每条C语句都必须在翻译环境中被其他程序转化为一系列的汇编指令,然后这些指令按照格式打好包,并以二进制磁盘文件的形式存放起来。
在ANSI C的任何一种实现中,存在两个不同的环境
- 翻译环境,在这个环节中源代码被转换成可执行的机器指令
- 执行环境,用于实际执行代码
翻译环境是由编译和链接两个过程组成的,而编译又可以细分为预处理(预编译)、编译和汇编三个过程
二、编译和链接
在一个项目中可能同时存在多个.c文件,这些文件经过编译器处理编译出对应的目标文件,再和链接库一起经过链接器处理生成最终的可执行程序
2.1 预处理(预编译)
在预处理阶段,源文件和头文件会被处理成以 .i 为后缀的文件
我们用gcc编译器观察一下对 test.c 文件预处理后的 .i 文件,在终端输入以下指令:
gcc -E test.c -o test.i
预处理阶段主要处理源文件中以#为开头的预编译指令,例如#include,#define,处理的规则如下
(1)将所有的#define删除,并展开所有的宏定义
图中我们可以观察到,在 test.i 中#define指令已经被删除,M也被替换成了100
(2)处理所有的条件编译指令,如 #if ,#ifdef,#elif,#else,#endif
(3)处理 #include 预编译指令,将包含的头文件的内容插入到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的头文件也可能包含其他头文件
(4)删除所有的注释
(5)添加行号和文件名标识,方便后续编译器生成调试信息等
(6)保留所有的 #pragma 编译器指令,因为后续编译器会用到
2.2 编译
编译器将预处理完的 .i 文本文件进行一系列的词法分析、语法分析、语义分析和优化,编译成以 .s 为后缀的汇编代码文件。汇编过程的命令如下:
gcc -S test.i -o test.s
汇编代码文件大概长这样:
假设有代码如下,编译过程该如何对其进行分析呢?
array [index] = (index+ 4 )*( 2 + 6 );
编译过程可分为6步:扫描(词法分析)、语法分析、语义分析、源代码优化、代码生成、目标代码优化。
(1)词法分析
将源代码程序输入扫描器中,扫描器将其中的字符序列分割成一系列的记号。上面的代码进行词法分析后得到了16个记号:
记号 | 类型 |
---|---|
array | 标识符 |
[ | 左方括号 |
index | 标识符 |
] | 右方括号 |
= | 赋值 |
( | 左圆括号 |
index | 标识符 |
+ | 加号 |
4 | 数字 |
) | 右圆括号 |
* | 乘号 |
( | 左圆括号 |
2 | 数字 |
+ | 加号 |
6 | 数字 |
) | 右圆括号 |
(2)语法分析
接下来,语法分析器将对扫描器生成的记号进行语法分析,产生语法树。
(3)语义分析
语义分析器对表达式进行语法层面分析。编译器所能做的分析是语义的静态分析。静态语义分析通常包括声明和类型的匹配,类型转换等。这个阶段会报告错误的语法信息
2.3 汇编
汇编器将汇编代码翻译成机器可执行的指令,每一条汇编语句几乎都对应一条机器指令。在这一步中,只是根据汇编指令和机器指令的对照表一一的进行翻译,不做指令优化
汇编会生成 .o 为后缀的目标文件,命令如下:
gcc -c test.s -o test.o
汇编后的目标文件就已经是二进制形式了,我们很难阅读
2.4 链接
目标文件在经过链接之后才能称为可执行文件。既然目标文件和可执行文件都是二进制格式,为什么还要再链接一次呢?
因为编译只是将我们自己写的代码变成了二进制格式,它还需要和标准库、动态链接库等结合起来,这些组件都是程序运行所必须的。
链接过程主要包括地址和空间分配,符号决议和重定位等步骤,解决的是一个项目中多文件、多模块之间互相调用的问题。
完.