承接《彻底理解链接器:一》
目录
符号表存放在哪里
符号决议的过程
实例说明undefined reference
过程二:库、可执行文件的生成
静态库
静态连接
静态链接下可执行文件的生成
在目标文件里有什么这一小节中,我们将一个目标文件简单的划分了两段,数据段和代码段,现在我们要向目标文件中再添加一段,而符号表也被编译器很贴心的放在目标文件中,因此一个目标文件可以理解为如图所示的三段,而符号表中的内容就是上一节当中编译器统计的表格。
有了符号表,链接器就可以进行符号决议了。
在上一节符号表中,我们知道符号表给链接器提供了两种信息,一个是当前目标文件可以提供给其它目标文件使用的符号,另一个其它目标文件需要提供给当前目标文件使用的符号。有了这些信息链接器就可以进行符号决议了。如图所示,假设链接器需要链接三个目标文件:
链接器会依次扫描每一个给定的目标文件,同时链接器还维护了两个集合,一个是已定义符号集合D,另一个是未定义符合集合U,下面是链接器进行符合决议的过程:
1,对于当前目标文件,查找其符号表,并将已定义的符号并添加到已定义符号集合D中。
2,对于当前目标文件,查找其符号表,将每一个当前目标文件引用的符号与已定义符号集合D进行对比,如果该符号不在集合D中则将其添加到未定义符合集合U中。
3,当所有文件都扫描完成后,如果为定义符号集合U不为空,则说明当前输入的目标文件集合中有未定义错误,链接器报错,整个编译过程终止。
上面的过程看似复杂,其实用一句话概括就是只要每个目标文件所引用变量都能在其它目标文件中找到唯一的定义,整个链接过程就是正确的。
如果你觉得上面的解释比较晦涩的话,你也可以将链接符号决议这个过程想象成如下的游戏:
新学期开学后,幼儿园的小朋友们都带了礼物要和其它的小朋友们分享,同时每个小朋友也有自己的心愿单,每个小朋友都可以依照自己的心愿单去其它的小朋友那里拿礼物,整个过程结束后,每个小朋友都能拿到自己想要的礼物。
在这个游戏当中,小朋友就好比目标文件,每个小朋友自己带的礼物就好比每个目标文件的已定义符号集合,心愿单就好比每个目标文件中未定义符号的集合。
假设我们写了一个math.c的数字计算程序,其中定义了一个add函数,该函数在main.c中被引用到,那么很简单,我们只需要在main.c中include写好的math.h头文件就可以使用add函数了,如图所示:
但是由于粗心大意,一不小心把math.c中的add函数给注释掉了,当你在写完main.c、打算很潇洒的编译一下时,出现了很经典的undefined reference to `add(int, int)`错误,如图所示 :
这个错误其实是这样产生的:
1, 链接器发现了你写的代码math.o中引用了外部定义的add函数(不要忘了,这是通过检查目标文件math.o中的符号表得到的信息),所以链接器开始查找add函数到底是在哪里定义的。
2,链接器转而去目标文件math.o的目标文件符号表中查找,没有找到add函数的定义。
3,链接器转而去其它目标文件符号表中查找,同样没有找到add函数的定义。
4,链接器在查找了所有目标文件的符号表后都没有找到add函数,因此链接器停止工作并报出错误undefined reference to `add(int, int)',如上图所示。
因此如果你很清楚链接器符号决议这个过程的话就会进行如下排查:
1:main.c中对add函数的函数名有没有写正确。
2:链接命令中有没有包含math.o,如果没有添加上该目标文件。
3:如果链接命令没有问题,查看math.c中定义的add函数定义是否有问题。
4:如果是C和C++混合编程时,确保相应的位置添加了extern "C"。
一般情况下经过这几个步骤的排查基本能够解决问题。
所以当你再次看到undefined reference这样的错误的是时候,你就应该可以很从容的去解决这类问题了。
在链接器可操作的元素这一节中我们提到,链接器可以操作的最小单元为目标文件,也就是说我们见到的无论是静态库、动态库、可执行文件,都是基于目标文件构建出来的。目标文件就好比乐高积木中最小的零部件。
给定目标文件以及链接选项,链接器可以生成两种库,分别是静态库以及动态库,如图所示,给定同样的目标文件,链接器可以生成两种不同类型的库,接下来我们分别介绍。
假设这样一个应用场景,基础设计团队设计了好多实用并且功能强大的工具函数,业务团队需要用到里面的各种函数。每次新添加其中一个函数,业务团队都要去找相应的实现文件并修改链接选项。使用静态库就可以解决这个问题。静态库在Windows下是以.lib为后缀的文件,Linux下是以.a为后缀的文件。
为解决上述问题,基础设计团队可以提前将工具函数集合打包编译链接成为静态库提供给业务团队使用,业务团队在使用时只要链接该静态库就可以了,每次新使用一个工具函数的时候,只要该函数在此静态库中就无需进行任何修改。
你可以简单的将静态库理解为由一堆目标文件打包而成, 使用者只需要使用其中的函数而无需关注该函数来自哪个目标文件(找到函数实现所在的目标文件是链接器来完成的,从这里也可以看出,不是所有静态库中的目标文件都会用到,而是用到哪个链接器就链接哪个)。静态库极大方便了对其它团队所写代码的使用。
静态库是链接器通过静态链接将其和其它目标文件合并生成可执行文件的,如下图一所示,而静态库只不过是将多个目标文件进行了打包,在链接时只取静态库中所用到的目标文件,因此,你可以将静态链接想象成如下图2所示的过程。
静态库是使用库的最简单的方法,如果你想使用别人的代码,找到这些代码的静态库并简单的和你的程序链接就可以了。静态链接生成的可执行文件在运行时不依赖任何其它代码,要理解这句话,我们需要知道静态链接下,可执行文件是如何生成的。
在上一节中我们知道,可以将静态链接简单的理解为链接器将使用到的目标文件集合进行拼装,拼装之后就生成了可执行文件,同时我们在目标文件里有什么这一节中知道,目标文件分成了三段,代码段,数据段,符号表,那么在静态链接下可执行文件的生成过程如图所示:
从上图中我们可以看到可执行文件的特点:
可执行文件和目标文件没有什么本质的不同,可执行文件区别于目标文件的地方在于,可执行文件有一个入口函数,这个函数也就是我们在C语言当中定义的main函数,main函数在执行过程中会用到所有可执行文件当中的代码和数据。而这个main函数是被谁调用执行的呢,答案就是操作系统(Operating System),这也是后面文章当中要重点介绍的内容。
现在你应该对可执行文件有一个比较形象的认知了吧。你可以把可执行文件生成的过程想象成装订一本书,一本书中通常有好多章节,这些章节是你自己写的,且一本书不可避免的要引用其它著作。静态链接这个过程就好比不但要装订你自己写的文章,而且也把你引用的其它人的著作也直接装订进了你的书里,这里不考虑版权问题 :),这些工作完成后,只需要按一下订书器,一本书就制作完成啦。
在这个比喻中,你写的各个章节就好比你写的代码,引用的其它人的著作就好比使用其它人的静态库,装订成一本书就好比可执行文件的生成。
静态链接是使用库的最简单最直观的形式, 从静态链接生成可执行文件的过程中可以看到,静态链接会将用到的目标文件直接合并到可执行文件当中,想象一下,如果有这样的一种静态库,几乎所有的程序都要使用到,也就是说,生成的所有可执行文件当中都有一份一模一样的代码和数据,这将是对硬盘和内存的极大浪费,假设一个静态库为2M,那么500个可执行文件就有1G的数据是重复的。如何解决这个问题呢,答案就是使用动态库。
接下来关于动态库,动态链接,已经动态链接和静态链接的对比等内容,我会在该系列的下一篇文章中介绍,如果你喜欢这个系列的文章,也欢迎关注我的微信公共账号,码农的荒岛求生,获取更多内容。
彻底理解链接器系列
彻底理解操作系统系列