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的书写顺序

 
  1. # gcc -o a.out main.o liba.a libb.a 
  2.  
  3. ...undefined reference... 
  4. error: ld returned 1 exit status  
 
  1. # 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:

 
  1.         E               U               D          
  2. +---------------+---------------+---------------+ 
  3. |     main.o    |    a2_func    |               | 
  4. +---------------+---------------+---------------+ 
  5. |               |    b1_func    |               | 
  6. +---------------+---------------+---------------+ 

接下来,输入liba.a,链接器发现,a2_func存在于liba.a的a2.o中,于是将a2.o加入到E,并在D中加入a2.o中所有定义的符号,其中包括a2_func,最后移除U中的a2_func,因为这个符号已经在a2.o中找到了的。然而,U中还有b1_func,所以连接还没有完成。

 
  1.        E               U               D          
  2. +---------------+---------------+---------------+ 
  3. |     main.o    |               |    a2_func    | 
  4. +---------------+---------------+---------------+ 
  5. |     a2.o      |    b1_func    | a2_func_other | 
  6. +---------------+---------------+---------------+ 

接着,输入libb.a,同理,链接器发现b1_func定义在b1.o中,所以在E中加入b1.o,移除U中的b1_func,在D中加入b1.o里面所有定义的符号

 
  1.         E               U               D          
  2. +---------------+---------------+---------------+ 
  3. |     main.o    |               |    a2_func    | 
  4. +---------------+---------------+---------------+ 
  5. |     a2.o      |               | a2_func_other | 
  6. +---------------+---------------+---------------+ 
  7. |     b1.o      |               |    b1_func    | 
  8. +---------------+---------------+---------------+ 

然而,由于b1.o调用到a1.o中的函数,我们假设是a1_func,但在D中并没有找到这个函数,所以a1_func还需要加入到U中

 
  1.         E               U               D          
  2. +---------------+---------------+---------------+ 
  3. |     main.o    |               |    a2_func    | 
  4. +---------------+---------------+---------------+ 
  5. |     a2.o      |               | a2_func_other | 
  6. +---------------+---------------+---------------+ 
  7. |     b1.o      |    a1_func    |    b1_func    | 
  8. +---------------+---------------+---------------+ 

但是,输入结束了!链接器发现U中还有未解析的符号,所以报错了!

可以看到由于链接器的算法实现,导致a1.o并没有被链接器考察,所以产生了未解析符号。仔细分析,可以知道,只要将liba.a和libb.a换一下顺序,就可以链接成功!

解决办法

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值