第二章、编译和链接

IDE和编译器提供的默认配置、编译和链接参数对于大部分的应用程序开发而言已经足够使用。但是在这样的开发过程中,我们往往会被这些复杂的集成工具所提供的的强大功能所诱惑,很多系统软件的运行机制与机理被掩盖,程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。

被隐藏了的过程

简单的的“Hello World”程序,在Linux下,当使用GCC来编译时

1
2
3
$gcc hello.c
$./a.out
Hello World!

事实上,上述过程可以分解为4个步骤,分别是

  • 预处理(prepressing)
  • 编译 (compilation)
  • 汇编 (assembly)
  • 链接 (link)

"1"

预编译

预编译讲stdio.h等被预编译成一个.i文件。C++程序的头文件可能是.cpp或.cxx,预编译后的文件扩展名是.ii。第一步预编译相当于如下命令(-E表示只预编译)

1
$gcc -E hello.c -o hello.i

预编译过程主要处理那些源代码文件中以“#”开始的预编译指令,比如“#include”、“#define”等,主要规则如下:

  • 将所有“#define”删除,并且展开所有的宏定义。
  • 处理所有条件预编译指令,比如 #if、#indef、#elif、#else、#endif。
  • 处理#indclude预编译指令,将被包含的文件插入到该预编译指令的位置。
  • 删除所有的注释 “//”和“/**/”。
  • 添加行号和文件名标识,以便于编译时编译器产生调试编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
  • 保留所有的#pragma编译器指令。

当无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

编译

编译过程就是把预处理的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件,这个过程往往是整个程序的核心。

1
$gcc -S hello.i -o hello.s

实际上gcc这个命令只是这些后台程序的包装,它会根据不同的参数要求去调用预编译编译程序cc1、汇编器as和链接器ld。

汇编

汇编器试讲汇编代码转变成机器可以执行的指令。

1
2
3
4
5
$as hello.s -o hello.o
or
$gcc -c hello.s -o hello.l
or
$gcc -c hello.c -o hello.o

链接

1
$ld -static 1.o 2.o -lgcc

编译器做了什么

编译器是将高级语言翻译成机器语言的一个工具。高级语言使程序开发者可以更加关注程序逻辑的本身,而尽量少考虑计算机本身的限制,如字长、内存大小、通信方式、存储方式等。高级语言的开发效率大大提高。
编译过程一般分为6步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。整个过程如图所示:

"2"

词法分析

源代码source code被输入到扫描器(Scanner),扫描器的任务很简单,就是简单的进行语法分析,将代码的字符序列分割成一系列的记号(Token)。词法分析产生的记号一般可以分为如下几类:关键字、标识符、字符量(包含数字、字符串等)和特殊符号(如加好、等号)

语法分析

语法分析器(Grammer Parser)将对扫描器产生的记号进行语法分析,从而产生语法书(Syntax Tree)。整个过程采用了上下文无关语法(Contex-free Grammer)。由语法分析器生成的语法书就是以表达式(Expression)为节点的数。

"3"

语义分析

语法分析仅仅是完成了对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意义。编译器所能分析的是静态语义(Static Semantic),所谓静态语义是指在编译期可以确定的语义,与之对应的是动态语义(Dynamic Semantic)就是只要在运行期间才能确定的语义。
静态语义通常包括声明和类型的匹配。
"4"

中间语言的生成

现代的编译器有着很多层次的优化,往往在源代码级别会有一个优化过程。我们这里所描述的源码优化器(Source Code Optimizer)在不同编译器中可能会有不同的定义或有一些其他的差异。源代码级优化器会在源代码级别进行优化。如上例中的表达式(2+6)这个表达式可能会优化掉,因为它的值在编译器就可以被确定。类似的还要其他的复杂的优化过程。经过优化后的语法书如下图所示:

"5"

直接在语法树进行优化比较困难,所以源代码优化器往往将整个语法书转换成中间代码(Intermediate Code), 它是语法树的顺序表示。中间代码有很多种类型,在不同的编译器中有着不同的形式,比较常见的是:三地址码(Three-address Code)和P-代码,最基本的三地址码是这样的:

1
x = y op z

比如
1
2
3
4
t1 = 2 + 6
t2 = index + 4
t3 = t2 * t1
array[index] = t3

为了使所有的操作都符合三地址码的形式,这里利用了几个临时变量,t1,t2和t3,经过优化:
1
2
3
t2 = index + 4
t2 = t2 * 8
array[index] = t2

中间代码可以使得编译器分为前端和后端。编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。

目标代码生成与优化

编译器后端主要包括代码生成器(Code Generate)和目标代码优化器(Target Code Optimizer)。

链接器

链接器年龄比编译器长

模块本质-静态链接

链接的主要内容是把各个模块直接相互引用的部分都处理好,使得各个模块直接能够正确的衔接。链接过程主要包括了地址和空间分配、符号决议和重定位。

"6"

最基本的链接过程如图所示。