程序编译概述
计算机语言演变:
机器语言——>汇编语言——>高级语言(机器语言或汇编语言的程序依赖于特定的机器)
机器指令0/1序列(目标程序)——>汇编指令——>源代码
由源程序到可运行程序的生成过程:
源代码文件——>汇编文件——>目标文件——>可执行程序
编译器 汇编器 链接器
hello.c——>hello.i——>hello.s——>hello.o——>hello.out/hello.exe
(gcc命令是后台程序的包装 编译驱动程序,会根据不同的参数要求去调用预编译程序、汇编器as、链接器Id)
1.预编译
预编译器cpp处理源文件的符号、宏扩展、#include扩展和其他预处理器指令,删除注释等,预编译不会生成文件,需要重定向,得到的仅是真正的源代码
2.编译
把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成汇编文件
3.汇编
汇编器as将汇编代码转变为机器可执行的机器指令,每条汇编指令对应一条机器指令,并将结果保存在二进制目标文件.o中
4.链接
将目标文件和多个二进制库文件链接成一个可执行程序
最常见的库是运行时库(Runtime Library),支持程序运行的基本函数的集合。库是一组目标文件的包,一些最常用的代码编译成目标文件后打包存放。
函数库一般分为静态库和动态库:(1)静态库.lib/.a,是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时就不再需要库文件;(2)动态库.dll/.so,在程序执行时由运行时链接文件加载库,这样节省系统开销。
编译包括以下几个阶段:
词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成
1.词法分析
词法分析器逐个字符对源程序进行扫描,将源代码的字符序列分割成一系列的记号token,一般分为:关键字、标识符、常数、运算符和界符。
LEX可根据用户描述的词法规则自动生成词法分析器
2.语法分析
编译的核心部分,对token采用上下文无关语法分析方法,检查token是否符合源语言的语法规则,生成抽象语法树(Abstract Syntax Tree)。
AST是以表达式为节点的树
YACC可根据用户描述的语法规则自动生成语法分析器
3.语义分析
判断语句是否真正有意义,如两个指针做乘法,编译器只能分析静态语义
(1)静态语义:编译期可确定的语义,eg声明和类型的匹配,类型的转换
(2)动态语义:只有在运行期才能确定的语义,eg将0作为除数
4.中间代码生成
将语法树转换成中间代码,不用的编译器有不同的形式,如三地址码和P-代码
5.代码优化
6.目标代码生成
将中间代码转换成目标机器代码,依赖目标机器
中间代码使得编译器结构可分为三阶段:前端、优化器、后端
- 前端:负责产生机器无关的中间代码,通常只针对一种编程语言或者一族类似的
- 优化器:负责对中间代码进行优化,独立于源语言和目标架构
- 后端(代码生成器):负责将中间代码转换成目标机器代码
为什么需要链接?汇编器不直接输出可执行文件?
程序由多个模块组成,模块之间需要交互组合形成程序,模块间最常见的通信方式有模块间函数调用和变量访问,这需要知道目标函数和目标变量的地址。如果目标代码中有变量定义在其他模块或调用其他模块的函数,但每个模块都是单独编译,因此编译器会把这些地址搁置,链接时等链接器确定这些地址。因此编译器将源代码编译成未链接的目标文件,由链接器将目标文件链接形成可执行文件。
为什么有预编译过程?
以#inlucde<.h>为例说明。假设a.cpp中定义全局函数a(),而b.cpp需要调用a(),可以在b.cpp中声明函数,编译器在编译时会生成符号表,存放引用其他模块的函数或变量,链接时编译器在其他目标文件中寻找这些符号的定义。若要调用成千上百个函数,在源文件中声明每个函数不现实,因此引入头文件.h,把所有函数声明放在头文件中,当源文件需要时,通过宏命名#include将这些内容合并到源文件中。编译器在编译前,通过预编译将头文件的声明加到源代码中。.h头文件中,只存放变量或函数的声明,本身不参与编译,但其内容在多个.cpp中被编译。
链接主要包括:地址和空间分配、符号决议和重定位(即把一些指令对其他符号地址的引用加以修正)