C语言的静态连接,简单的说就是将编译得到的目标文件.o(.obj),打包在一起,并修改目标文件中函数调用地址偏移量的过程。当在大一点的项目中,可能会遇到连接时,由于静态库在链接器命令行中出现顺序的问题,造成undefined reference错误。本文深入探讨一下这个问题,以及如何解决。
问题
如下图。假设有这么一个场景,在我们的构建系统中,构建了一个两个静态库文件liba.a和libb.a,其中liba.a包含两个目标文件a1.o和a2.o,而libb.a包含一个目标文件b1.o。希望将main.o静态连接liba.a和libb.a。
注意到黄色的箭头表示调用关系:b1.o需要调用a1.o中的某函数,而main.o调用了a2.o和b1.o中的函数。你可以把.o文件理解为对应的.c文件。
那么如下的两个命令哪个会成功执行呢?注意到这两个命令唯一的区别是对liba.a和libb.a的书写顺序
- # gcc -o a.out main.o liba.a libb.a
- ...undefined reference...
- error: ld returned 1 exit status
- # gcc -o a.out main.o libb.a liba.a
静态连接的算法
要理解上面这个问题,需要理解链接器在处理静态连接时候的算法。此处的阐述参考《深入理解计算机系统》中的“链接”章节。
首先,需要明确的是,链接器在考察库文件(.a)的时候,不是把库文件看做一个整体,而是将打包在其中的目标文件(.o)作为考察单元。在整个连接过程中,如果某个目标文件中的符号被用到了,那么这个目标文件会单独从库文件中提取出来,而不会把整个库文件连接进来。
然后,链接器在工作过程中,维护3个集合:需要参与连接的目标文件集合E、一个未解析符号集合U、一个在E中所有目标文件定义过的所有符号集合D。
以上面第一条命令gcc -o a.out main.o liba.a libb.a为例,我们来一步步看看链接器的工作过程:
当输入main.o后,由于main调用了a2.o和b1.o中的函数,而此时并没有在D中找到该符号,于是将引用的两个函数保存在U中,此处假设两个函数分别为a2_func和b1_func:
- E U D
- +---------------+---------------+---------------+
- | main.o | a2_func | |
- +---------------+---------------+---------------+
- | | b1_func | |
- +---------------+---------------+---------------+
接下来,输入liba.a,链接器发现,a2_func存在于liba.a的a2.o中,于是将a2.o加入到E,并在D中加入a2.o中所有定义的符号,其中包括a2_func,最后移除U中的a2_func,因为这个符号已经在a2.o中找到了的。然而,U中还有b1_func,所以连接还没有完成。
- E U D
- +---------------+---------------+---------------+
- | main.o | | a2_func |
- +---------------+---------------+---------------+
- | a2.o | b1_func | a2_func_other |
- +---------------+---------------+---------------+
接着,输入libb.a,同理,链接器发现b1_func定义在b1.o中,所以在E中加入b1.o,移除U中的b1_func,在D中加入b1.o里面所有定义的符号
- E U D
- +---------------+---------------+---------------+
- | main.o | | a2_func |
- +---------------+---------------+---------------+
- | a2.o | | a2_func_other |
- +---------------+---------------+---------------+
- | b1.o | | b1_func |
- +---------------+---------------+---------------+
然而,由于b1.o调用到a1.o中的函数,我们假设是a1_func,但在D中并没有找到这个函数,所以a1_func还需要加入到U中
- E U D
- +---------------+---------------+---------------+
- | main.o | | a2_func |
- +---------------+---------------+---------------+
- | a2.o | | a2_func_other |
- +---------------+---------------+---------------+
- | b1.o | a1_func | b1_func |
- +---------------+---------------+---------------+
但是,输入结束了!链接器发现U中还有未解析的符号,所以报错了!
可以看到由于链接器的算法实现,导致a1.o并没有被链接器考察,所以产生了未解析符号。仔细分析,可以知道,只要将liba.a和libb.a换一下顺序,就可以链接成功!
解决办法
一般来说有两种办法,一种是仔细分析依赖关系,并按照正确的顺序书写库文件的引用。原则是被依赖的尽量写在右边。但是在有些大型项目中,依赖关系可能并不容易梳理清楚。此时可以在命令行参数中重复对库文件的引用:
- # gcc -o a.out main.o liba.la libb.la liba.a
在上面的命令中,liba.a重复书写了两次。
如果你使用automake,可以用xxx_LIBADD和xxx_LDADD来控制目标文件的引用关系:
- xxx_LIBADD:对于目标文件为库文件或可执行文件,需使用这个选项。表示在打包目标库文件的时候,就将依赖的文件一并打包进来。
- xxx_LDADD:对于可执行文件可用这个选项,来控制链接器的参数,如果你能分析清楚依赖关系,可以在这个选项中按照正确的顺序书写,从而成功连接。