从源代码到可执行文件

一个C语言程序的生命是从源文件开始的,这种高级语言的形式更容易被人理解。然而,要想在操作系统上运行程序,每条C语句都必须被翻译为一系列的低级机器语言指令。最后,这些指令按照可执行目标文件的格式打包,并以二进制文件的形式存放起来。

我们首先回顾编译原理的基础知识,然后以经典著作The C Programming Language中的第一个程序hello world为例,讲解Linux下默认编译器GCC(版本5.4.0)的编译过程。

1.编译原理

编译器的作用是读入以某种语言(源语言)编写的程序,输出等价的用另一种语言(目标语言)编写的程序。编译器的结构可分为前端(Front end)和后端(Back end)两部分。前端是机器无关的,其功能是把源程序分解成组成要素和相应的语法结构,通过这个结构创建源程序的中间表示,同时收集和源程序相关的信息,存放到符号表中;后端则是机器相关的,其功能是根据中间表示和符号表信息构造目标程序。

编译过程可大致分为下面5个步骤,如图11所示。

(1)词法分析(Lexical analysis):读入源程序的字符流,输出为有意义的词素(Lexeme);

(2)语法分析(Syntax analysis):根据各个词法单元的第一个分量来创建树型的中间表示形式,通常是语法树(Syntax tree);

(3)语义分析(Semantic analysis):使用语法树和符号表中的信息,检测源程序是否满足语言定义的语义约束,同时收集类型信息,用于代码生成、类型检查和类型转换;

(4)中间代码生成和优化:根据语义分析输出,生成类机器语言的中间表示,如三地址码。然后对生成的中间代码进行分析和优化;

(5)代码生成和优化:把中间表示形式映射到目标机器语言。

图1 编译过程

2.GCC编译过程

首先我们来看GCC的编译过程,hello.c的源代码如下。

在编译时添加“-save-temps”和“–verbose”编译选项,前者用于将编译过程中生成的中间文件保存下来,后者用于查看GCC编译的详细工作流程,下面是几条最关键的输出。

可以看到,GCC的编译主要包括四个阶段,即预处理(Preprocess)、编译(Compile)、汇编(Assemble)和链接(Link),如图2所示,该过程中分别使用了cc1、as和collect2三个工具。其中cc1是编译器,对应第一和第二阶段,用于将源文件hello.c编译为hello.s;as是汇编器,对应第三阶段,用于将hello.s汇编为hello.o目标文件;链接器collect2是对ld命令的封装,用于将C语言运行时库(CRT)中的目标文件(crt1.o、crti.o、crtbegin.o、crtend.o、crtn.o)以及所需的动态链接库(libgcc.so、libgcc_s.so、libc.so)链接到可执行hello。

图2 GCC的编译阶段

3.预处理阶段

GCC编译的第一阶段是预处理,主要是处理源代码中以“#”开始的预处理指令,比如“#include”、“#define”等,将其转换后直接插入程序文本中,得到另一个C程序,通常以“.i”作为文件扩展名。在命令中添加编译选项“-E”可以单独执行预处理:

hello.i文件的内容如下所示。

通过观察我们可以得知预处理的一些处理规则,如下。

递归处理“#include”预处理指令,将对应文件的内容复制到该指令的位置;删除所有的“#define”指令,并且在其被引用的位置递归地展开所有的宏定义;处理所有条件预处理指令:“#if”、“#ifdef”、“#elif”、“#else”、“#endif”等;删除所有注释;添加行号和文件名标识。

4.编译阶段

GCC编译的第二阶段是编译,该阶段将预处理文件进行一系列的词法分析、语法分析、语义分析以及优化,最终生成汇编代码。在命令中添加编译选项“-S”,操作对象可以是源代码hello.c,也可以是预处理文件hello.i。实际上在GCC的实现中,已经将预处理和编译合并处理。

GCC默认使用AT&T格式的汇编语言,添加编译选项“-masm=intel”可以将其指定为我们熟悉的intel格式。编译选项“-fno-asynchronous-unwind-tables”则用于生成没有cfi宏的汇编指令,以提高可读性。hello.s文件的内容如下所示。

值得注意的是,生成的汇编代码中函数printf()被替换成了puts(),这是因为当printf()只有单一参数时,与puts()是十分类似的,于是GCC的优化策略就将其替换以提高性能。

5.汇编阶段

GCC编译的第三阶段是汇编,汇编器根据汇编指令与机器指令的对照表进行翻译,将hello.s汇编成目标文件hello.o。在命令中添加编译选项“-c”,操作对象可以是hello.s,也可以从源代码hello.c开始,经过预处理、编译和汇编直接生成目标文件。

此时的目标文件hello.o是一个可重定位文件(Relocatable File),可以使用objdump命令来查看其内容。

此时由于还未进行链接,对象文件中符号的虚拟地址无法确定,于是我们看到字符串“hello,world.”的地址被设置为0x0000,作为参数传递字符串地址的rdi寄存器被设置为0x0,而“call puts”指令中函数puts()的地址则被设置为下一条指令的地址0xe。

6.链接阶段

GCC编译的第四阶段是链接,可分为静态链接和动态链接两种。GCC默认使用动态链接,添加编译选项“-static”即可指定使用静态链接。这一阶段将目标文件及其依赖库进行链接,生成可执行文件,主要包括地址和空间分配(Address and Storage Allocation)、符号绑定(Symbol Binding)和重定位(Relocation)等操作。

链接操作由链接器(ld.so)完成,结果就得到了hello文件,这是一个静态链接的可执行文件(Executable File),其包含了大量的库文件,因此我们只将关键部分展示如下。

可以看到,通过链接操作,对象文件中无法确定的符号地址已经被修正为实际的符号地址,程序也就可以被加载到内存中正常执行了。

举报/反馈

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片