编译和链接
2.1被隐藏的过程
#include<Stdio.h>
int main()
{
printf("Hello World")
return 0;
}
我们用VS编译这段代码的时候只需右键项目文件,点击 生成 就可以生成exe文件。事实上,上述过程可分成四个步骤,分别是 预编译(Propressing), 编译(Compilation),汇编(Assembly),链接(Linking)。
2.1.1预编译
首先是hello.c文件和stdio.h文件被预编译成一个.i文件。预编译过程主要处理那些已“#”开头的代码,比如“#define”、“#include”等。主要规则如下:
- 将所有的#define删除,并展开所有宏定义
- 处理所有的预编译指令,如#if、 #elif、 #endif等。
- 处理#include指令,将该包含文件插入到指令所在的位置,这个过程是递归的,也就是说包含的文件可能还会包含文件。
- 删除所有的注释(// ,/**/)
- 添加行号和文件标识
- 保留所有的#pragma指令
2.1.2编译
编译是将预处理后的文件进行 词法分析、语法分析、语义分析及优化后产生汇编代码文件的过程。
2.1.3汇编
汇编器将汇编代码翻译成机器代码。每一条汇编指令几乎都对应一条机器指令。
2.1.4链接
链接器将目标文件和库文件链接成EXE文件。
2.2编译器做了什么
从最直观的角度看,编译器就是将高级语言翻译成机器语言的工具。比如我们可以使用C++写的程序,可以让编译器翻译成机器可以指定的指令和数据。
编译过程一般分为六个步骤:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。
扫描器的任务很简单,只是进行简单的词法分析。
词法分析产生的几号一般分为几类:关键字、标识符、字面量(数字、字符串等)和特殊符号(加号、减号等)。
2.2.2语法分析
语法分析器对扫描器产生的标记进行语法分析,并产生语法树,整个过程采用上线文无关的语法分析手段。语法分析器 产生的语法树是以表达式为节点的树,符号和数字是最小的表达式,通常作为语法树的叶子节点。在语法分析的同时很多运算符的优先级也被确定下来了。
另外,有些运算法有特殊意义,比如C++里的 * 既可以标识乘号,又可以标识解引用,所以语法分析必须对两种情况进行区分。如果出现了表达式不合法,比如括号不匹配或缺少操作符等,编译器会报告语法分析时错误。
2.2.3语义分析
语法分析仅仅是对表达式的语法层面的分析,但是它不知道这个语句是否有意义。比如两个指针做乘法运算是没意义的,但是它在语法分析时是合法的。
编译器多能分析的语义是静态语义,即在编译器可以确定的语义,与之对应的是动态语义,即在运行期才能确定的语义。
静态语义包括声明与类型匹配,类型转换。比如把一个浮点数赋值给一个整数,隐含一个浮点数到整数的转换,语义分析要完成这个步骤。比如把一个浮点数赋值给一个指针,语义分析会发现类型不匹配,编译器会报错。动态语义一般指运行时出现的语义相关的问题,比如除0问题是一个运行期语义错误。
经过语义分析阶段后,整个语法树的表达式都被标记了类型,如果某些类型涉及到隐式转换,语义分析程序会在语法树中插入相应的节点。
2.2.4中间语言生成
现代编译器会有很多层次的优化,在原始代码级别会有一个优化过程。源代码级优化器将语法树转换成中间代码,它是语法树的顺序表示。其实它已经很接近目标代码了,只是它和机器和运营环境无关,比如它不包含数据的尺寸,变量地址和寄存器名字等。比较常见的中间代码有:三地址码 和P-代码。三地址码的基本形式是:
x = y op z
中间代码是的编译器可以分成前端和后端。前端负责生成机器无关的中间代码,后端将中间代码转换成目标机器代码。这样对于一些可以跨平台的语言而言,可以针对多个平台使用一个前端,和针对不同平台的多个后端。
2.2.5目标代码生成与优化
源代码级优化器产生中间代码,标志着其后的步骤都属于编译器后端。编译器后端主要包括代码生成器和目标代码优化器。代码生成器将中间代码翻译成机器代码,这个过程十分依赖目标机器,因为不同的机器有着不同的字长、寄存器、整数类型 和 浮点数类型。目标代码优化器对目标代码进行优化,如去掉多余的指令、用移位代替乘法和选择合适的寻址方式。
2.3 链接器
编译好的目标代码里可能会引用其他模块的符号,链接器负责在各个模块之间查找被引用的符号。如果一个被引用的符号在所有模块里都没有找到 或者 多个模块定了相同的符号,链接器会报告错误。