从helloworld理解Linux的编译系统
本文将以一个简单的helloworld程序由编写到运行的整个过程来解释Linux的编译系统的原理。
首先,用C语言写一个程序hello.c
内容如下:
- #include<stdio.h>
- intmain()
- {
- printf("helloworld\n");
- }
保存后,用gcc进行编译:
gcc-ohellohello.c
然后执行编译后的hello程序:
./hello或者hello
执行结果如下:
helloworld
hello程序的生命周期是从一个高级C语言程序开始的,因为这种形式能够被人读懂。然而,为了在系统上运行hello.c程序,每条C语句都必须被其他程序转化为一系列的低级机器语言指令。然后这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来。目标程序也称为可执行目标文件。
在Unix系统上,从源文件到目标文件的转化是由编译器驱动程序完成的:
unix>gcc-ohellohello.c
在这里,gcc命令依次调用了多个程序(分别为:预处理器——>编译器(当然这里指GCC编译器)——>汇编器——>链接器),将源程序文件hello.c翻译成一个可执行目标文件hello。这个翻译的过程可分为四个阶段完成,如下图所示。执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统(compilationsystem)。
注意:gcc命令是调用多个程序,而不是仅调用GCC编译器,GCC编译器只是编译系统的一部分。具体参考下图:
编译系统
•预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第1行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩
展名。
•编译阶段。编译器(cc1)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通
用的输出语言。例如,C编译器和Fortran编译器产生的输出文件用的都是一样的汇编语言。
•汇编阶段。接下来,汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatableobjectprogram)的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它的字节编码是机器语言指令而不是字符。如果我们在文本编辑器中打开hello.o文件,看到的将是一堆乱码。
•链接阶段。请注意,hello程序调用了printf函数,它是每个C编译器都会提供的标准C库中的一个函数。printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。链接器(ld)就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。
说到这里,相信大家对Linux的编译系统已经有了初步的认识,那么,我们为什么要去深入的了解编译系统呢,主要原因如下:
• 优化程序性能。现代编译器都是成熟的工具,通常可以生成很好的代码。作为程序员,我们无需为了写出高效代码而去了解编译器的内部工作。但是,为了在C 程序中做出好的编码选择,我们确实需要了解一些机器代码以及编译器将不同的C 语句转化为机器代码的方式。例如,一个switch 语句是否总是比一系列的if-then-else 语句高效得多?一个函数调用的开销有多大? while 循环比for 循环更有效吗?指针引用比数组索引更有效吗?为什么将循环求和的结果放到一个本地变量中,与将其放到一个通过引用传递过来的参数中相比,运行速度要快很多呢?为什么我们只是简单地重新排列一下一个算术表达式中的括号就能让一个函数运行得更快?
• 理解链接时出现的错误。根据我们的经验,一些最令人困扰的程序错误往往都与链接器操作有关,尤其是当你试图构建大型的软件系统时。例如,链接器报告它无法解析一个引用,这是什么意思?静态变量和全局变量的区别是什么?如果你在不同的C 文件中定义了名字相同的两个全局变量会发生什么?静态库和动态库的区别是什么?我们在命令行上排列库的顺序有什么影响?最严重的是,为什么有些链接错误直到运行时才会出现?