程序编译概述

计算机语言演变:

机器语言——>汇编语言——>高级语言(机器语言或汇编语言的程序依赖于特定的机器)

机器指令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.目标代码生成

将中间代码转换成目标机器代码,依赖目标机器

中间代码使得编译器结构可分为三阶段:前端、优化器、后端

  1. 前端:负责产生机器无关的中间代码,通常只针对一种编程语言或者一族类似的
  2. 优化器:负责对中间代码进行优化,独立于源语言和目标架构
  3. 后端(代码生成器):负责将中间代码转换成目标机器代码

 

为什么需要链接?汇编器不直接输出可执行文件?

程序由多个模块组成,模块之间需要交互组合形成程序,模块间最常见的通信方式有模块间函数调用和变量访问,这需要知道目标函数和目标变量的地址。如果目标代码中有变量定义在其他模块或调用其他模块的函数,但每个模块都是单独编译,因此编译器会把这些地址搁置,链接时等链接器确定这些地址。因此编译器将源代码编译成未链接的目标文件,由链接器将目标文件链接形成可执行文件。

 

为什么有预编译过程?

以#inlucde<.h>为例说明。假设a.cpp中定义全局函数a(),而b.cpp需要调用a(),可以在b.cpp中声明函数,编译器在编译时会生成符号表,存放引用其他模块的函数或变量,链接时编译器在其他目标文件中寻找这些符号的定义。若要调用成千上百个函数,在源文件中声明每个函数不现实,因此引入头文件.h,把所有函数声明放在头文件中,当源文件需要时,通过宏命名#include将这些内容合并到源文件中。编译器在编译前,通过预编译将头文件的声明加到源代码中。.h头文件中,只存放变量或函数的声明,本身不参与编译,但其内容在多个.cpp中被编译。

 

链接主要包括:地址和空间分配、符号决议和重定位(即把一些指令对其他符号地址的引用加以修正)