编译与链接详解

前文:

我们知道一个.c/.cpp源程序文件要最后变成我们的.exe(windows)或者.out(Linux)可执行文件,要经过我们的编译和链接。了解这个过程对程序员来说是最基本的素质(因为写了这么久的代码都不知道它最后为啥能执行,那不是搞笑吗?)。

注:文章中红色字体是重要关键部分,蓝色字体是后面我的博客会详细讲解的内容,如果有兴趣可以关注,一起学习,并且揪出我的错误,甚是感激


正文:

首先总体分为了编译链接两大步。

   编译又包含了:预处理,编译,汇编,生成二进制可重定位文件。

           链接又包含了:

链接第一步:1.合并所有需要链接的.obj(.o)文件 的段,并且调整段的偏移和段长度,合并.o文件的符号表。2.符号的解析。3。分配符号的虚拟内存地址。

链接第二步:符号的重定位

在linux底下,可以通过下面的命令来获得,编译链接的每个阶段文件。

(1) 预编译   gcc -E a.c -o a.i  

(2) 编译     gcc -S a.i-o a.s   

(3) 汇编     gcc -c a.s -o a.o  

(4) 链接     gcc -o a.o -o a.out


预处理阶段:它为我们把源代码中展开了所有以“#”开头的宏定义,并且删除了所有的注释。预处理后生成我们的.i文件

编译阶段:

在这个过程中,源代码会被放入到一个扫描器中,扫描器会利用一种叫有限状态机的算法将里面的代码划分成单个记号,这些记号一般就是关键字,标识符类等。然后会进行语法分析,语法分析的过程中,会将扫描器中的记号用树状结构连接起来。然后进行一些运算符的优先级判断,以及表达式的错误判断,然后进行语义分析,语义分析就是对树状结构的记号加上类型,从而让编译器去执行一些隐示类型转换,以及类型的错误判断。接下来编译器会将源代码优化成我们的汇编语言。

   汇编阶段:通过汇编器将汇编语言转换成我们的计算机可以执行的二进制可重定位文件。为何是可重定位?

因为我们任何一个源文件在进行编译阶段的时候会去产生我们的符号表,符号表中存放的就是我们程序所产生的符号(例如:函数名,变量名等),我们的编译阶段是不会去给我们的符号分配正确的地址,因此当我们查看.obj(.o)文件的符号表信息时就会出现下面这种情况:

我们定义一个main函数:                                                            同时定义一个add函数:

                             

这时我们去编译main.c生成我们的.o文件,并且去查看.o文件的符号表:


我们可以发现我们符号的地址都是0x00000000,这些都是错误的地址,因此我们的计算机无法通过正确的地址去寻找到它的指令,那么计算机就无法去执行,这也就是我们.obj(.o)文件没有链接为何不能执行的根本原因.

在这里简单的介绍一下.o(.obj)文件组成:


首先在.o文件组成开头是一个ELF头部信息。它的作用是告知该文件的类型,版本号,程序的入口地址等基本信息,并且它还保留section table段的地址(可以使用readelf -h 命令来查看)。

Section table段:里面保存着.o文件其他段的属性,偏移量,段长度等信息。

.text段是指令段,可读可执行

.rodata段是只读数据段

.data是已经完成初始化和初始化不为0的数据段,可读可写

.bss未初始化,并且初始化为0的数据段,可读可写

还有很多段信息,在这不一一详解


链接第一步:

扫描所有的输入的.o文件,去获得它们各个段的长度,属性和位置,然后按照段的属性和段的长度合并,并且去把每个.o文件的符号全部收集起来放入同一个符号表中。在这里要注意的是我们在链接的时候不光是要去链接我们用户自己所写编译后的.o文件还有一些库函数中的(例如:printf.o),同时还会去链接我们的glibc的辅助库函数

例如crt1.o,crti.o,crtbegin.o,crtend.o,crtn.o

前面这5个目标文件的作用分别是启动、初始化、构造、析构和结束,它们通常会被自动链接到应用程序中

一般的链接顺序就是:ld ctr1.o ctri.o crtbegin.o用户的.o  系统库函数.o  ctrend.o  crtn.o
crt1.o中包含程序的入口函数_start以及两个未定义的符号__libc_start_main和main,由_start负责调用 __libc_start_main初始化libc,然后调用我们源代码中定义的main函数;

另外,由于类似于全局静态对象这样的代码需要在main函 数之前执行,crti.o和crtn.o负责辅助启动这些代码。
另外,Gcc中同样也有crtbegin.o和crtend.o两个文件,这两个目标文件 用于配合glibc来实现C++的全局构造和析构。

crt1.o是crt0.o的后续演进版本,crt1.o中会非常重要的.init段和.fini段以及_start函数的入口..init段和.fini段实际上是靠crti.o以及crtn.o来实现的.

 init段main函数之前的初始化工作代码, 比如全局变量的构造. 
fini段则负责main函数之后的清理工作.crti.o crtn.o是负责C的初始化,而C++则必须依赖crtbegin.o和crtend.o来帮助实现.

然后是我们的符号解析,符号解析的过程就是一些符号还处于UND状态,说明我们只是在引用它们,并未找到它们定义的地方,这时链接器就会去找它们正确定义的地方,如果没找到那么就是链接报错,在找的过程中,可能涉及到强符号和弱符号,一般编译器会以强符号定义为准,找到之后我们就要给这些符号分配正确的虚拟地址。

链接的第二步:符号的重定位,经历过链接第一步,我们的符号的地址已经被正确的分配,因此我们需要回到我们的.o文件的符号表中,去给这些符号修改成正确的地址,这样我们的计算机就能够正确的寻址,从而执行指令,达到我们的可执行文件。

        看似简单的编译和链接,其实底层也发生了很多事情,虚心学习,其实你懂的只是沧海一粟。只有不断的学习积累,才能有所成就。

更多的内容可以阅读:《程序员的自我修养》:第1,2,3,4,7章