September 15th, 2016

C语言的静态连接,简单的说就是将编译得到的目标文件.o(.obj),打包在一起,并修改目标文件中函数调用地址偏移量的过程。当在大一点的项目中,可能会遇到连接时,由于静态库在链接器命令行中出现顺序的问题,造成undefined reference错误。本文深入探讨一下这个问题,以及如何解决。

问题

如下图。假设有这么一个场景,在我们的构建系统中,构建了一个两个静态库文件liba.alibb.a,其中liba.a包含两个目标文件a1.oa2.o,而libb.a包含一个目标文件b1.o。希望将main.o静态连接liba.alibb.a

注意到黄色的箭头表示调用关系:b1.o需要调用a1.o中的某函数,而main.o调用了a2.ob1.o中的函数。你可以把.o文件理解为对应的.c文件。

那么如下的两个命令哪个会成功执行呢?注意到这两个命令唯一的区别是对liba.alibb.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_funcb1_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.alibb.a换一下顺序,就可以链接成功!

解决办法

一般来说有两种办法,一种是仔细分析依赖关系,并按照正确的顺序书写库文件的引用。原则是被依赖的尽量写在右边。但是在有些大型项目中,依赖关系可能并不容易梳理清楚。此时可以在命令行参数中重复对库文件的引用:

# gcc -o a.out main.o liba.la libb.la liba.a

在上面的命令中,liba.a重复书写了两次。

如果你使用automake,可以用xxx_LIBADD和xxx_LDADD来控制目标文件的引用关系:

  • xxx_LIBADD:对于目标文件为库文件或可执行文件,需使用这个选项。表示在打包目标库文件的时候,就将依赖的文件一并打包进来。
  • xxx_LDADD:对于可执行文件可用这个选项,来控制链接器的参数,如果你能分析清楚依赖关系,可以在这个选项中按照正确的顺序书写,从而成功连接。

1块2块也是钱,小额赞助