gcc的编译流程详解
c语言是编译型的语言,必须经过编译器的编译才能在内存中加载被处理器执行,从c语言的源文件到最终的处理器能够执行机器码,是我们通常所说的”编译“,这是个模糊的概念,实际上需要预处理、编译、汇编、链接四个处理流程,那么编译器在这四个过程中都做了哪些事情呢?实际上,从c语言到机器码涉及到c语言、汇编语言、机器语言,即是从从“人可识别”性好的高级语言到机器可识别的低级语言,还好编译器帮助我们做好了这些事情。
本文以gcc编译器为例来详解这四个过程,全文以最简单的 “helloworld” 程序为例里说明"编译"流程,由于涉及到晦涩难懂的汇编语言和只有机器才能识别的机器语言,所以大家不要深究汇编语言中的汇编指令的具体含义的,机器码如何和汇编指令对应的,只要能够理解这四个“编译”流程即可,对于如何产生机器码,怎样检查语法错误等都是由编译器自动来完成的,这里面涉及到的是编译原理,况且编译器对应与不同cpu体系架构,例如生成x86架构的机器码和生成arm架构的机器码对应的编译器是不一样的。了解了c语言的编译流程对于程序设计有很大帮助,有助于我们理解最底层的东西,应对一些未知的编译或运行错误,如段错误等。
以下是我们要实验的测试代码(c语言入门的helloworld程序,当然那个数组a对于程序执行来说毫无意义只是用于说明问题):
首先我们先使用最常用的gcc编译选项:gcc hello.c -o hello来编译,这时生成的只有最终的可执行文件 hello。
查下文件类型:file hello 发现其是elf格式(这个是linux所使用的二进制可执行文件的格式),且是能在x86架构上运行的可执行文件。
当然是可以执行的:./hello 执行 (hello前加 './' 的原因是代表当前目录下,如果不加会被当作shell命令来执行,产生报错信息)
可以看得到按照这种编译选项是直接执行了预处理、编译、汇编、链接四步,生成最终的可执行文件,没有中间文件的生成,接下来我们使用gcc的其他编译选项来详解gcc的四个“编译”流程。
一、预处理
预处理的含义就是在编译开始之前对源程序做一些初步的转换,这些都是有预处理器的预处理程序来完成转化的,主要完成三个主要任务:文件包含、宏替换、去注释。检查包含预处理指令的语句和宏定义,并对源代码进行相应的转化,预处理指令都是以#开头的代码行(如:#include #define等),关于预处理的一些内容将在以后的一些内容中有所涉及。
命令行输入:gcc -e hello.c -o hello.i 之后生成了hello.i的文件,这个文件就是预处理之后的文件。
打开hello.i 文件我们发现:都是一些文件路径的描述 ,变量的定义,结构体的定义,函数的声明等。
以上是一些文件路径的说明:
以上是变量类型的定义:
以上是结构体的定义(这里是标准io的file结构体的定义):
以上是函数的声明(这里是标准io的库函数接口的声明,可以看到我们使用的printf函数的声明),当然这里没有函数的定义,函数的定义实现是在libc库中实现,在链接的阶段会链接要使用的库函数:
上面都是文件包含(#include
二、编译
在这个阶段,编译器会把预处理之后的源文件 hello.i 编译成汇编语言 ,此时会对源文件进行语法检查,如果源文件中有语法错误,编译器会停止编译,并打印出错误信息供程序员检查修改。
命令行输入:gcc -s hello.i -o hello.s 会生成hello.s文件这个文件就是汇编文件。
打开hello.s如下:可见比原来的c文件多了不少行,虽然也不是太多,但是我们可能无法读的明白每条指令的含义。我们可以大致看到: .rodata这是个只读的数据段(其中第四行就是只读的“helloworld”),还会看到.text 这是个代码段就是可执行的二进制代码所在的位置等等。如果想搞明白每条指令的含义,不仅需要我们懂得底层的cpu架构的知识,还要懂得汇编语言的语法规则,其次要了解每条指令所对应的x86指令集。当然,每种架构所对应的汇编指令集是不同的,编译生成的汇编文件也是不一样的,幸运的是这些东西都不需要我们去转化,编译器会给我们做好这些转化工作。
三、汇编
经过编译器编译后生成的hello.s文件还是不能被执行的,因为不能被机器识别(只识别二进制文件),所以需要第三步的汇编阶段,汇编器会把hello.s文件汇编成目标机器指令(*.o文件)。因为每条汇编指令都对应与相应的二进制机器码,所以这样的转化对于汇编器来说是很简单的,一般目标代码都会有代码段和数据段。
命令行输入:gcc -c hello.s -o hello.o 就得到了目标文件hello.o文件
打开hello.o文件(如下):发现都是对我们来说的乱码,不过不用担心这是给机器读的,知道这些就可以了。
unix环境下主要有三种类型的目标文件:
(1)可重定位文件
其中包含了适合于其他目标文件链接来创建一个可执行的或共享的目标文件的代码和数据。
(2)共享的目标文件
这种文件存放了适合于在两种上下文里链接的代码和数据。第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个 目标文件;第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。
(3)可执行文件
它包含了一个可以被操作系统创建一个进程来执行之的文件。汇序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。
四、链接
由于汇编之后生成的目标文件不能立即被执行,其中可能还有尚未解决的问题,例如:某个源文件可能引用另一个源文件中的某些符号(变量或者函数调用等),程序中也有可能调用了某个库函数中的函数等等。这里就是调用了标准io库的printf函数。链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
命令行输入:gcc hello.o -o hello
打开hello文件(如下):还是我们不能识别的乱码,比目标文件“体积”大些,这是链接的结果,但是这是可以被操作系统加载执行的。
根据开发人员指定的同库函数的链接方式不通,链接处理可分为两种:
(1)静态链接
在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
(2)动态链接
在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。
至此我们就完成了从一个c源文件到可执行文件的过程,所写的代码归结到底层都会被转化为特定机器能够识别并执行的二进制机器码,当然大家还会有一些疑问:静态库、动态库如何链接和创建,预处理指令如何在代码中使用等等,将会在其他的博文中涉及到。最后需要说明的是:其中的案例代码都是本人实践和查阅权威资料得出,如有出入望提出宝贵意见。