原文链接:C静态库连接的顺序问题
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_LIBAD
D和xxx_LDADD
来控制目标文件的引用关系:
xxx_LIBADD:对于目标文件为库文件或可执行文件,需使用这个选项。表示在打包目标库文件的时候,就将依赖的文件一并打包进来。
xxx_LDADD:对于可执行文件可用这个选项,来控制链接器的参数,如果你能分析清楚依赖关系,可以在这个选项中按照正确的顺序书写,从而成功连接。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。