GCC/LD编译链接潜规则

gcc/ld的编译链接的有很多潜规则, 如果不幸遇到了, 并且没有意识到这是个潜规则, 那么会浪费很久很久的调试时间而不得其解. 下面总结我所遇到的一些潜规则.

本文先出第一弹, 其他的潜规则将陆续放出.


潜规则:

当一个符号在多个目标文件(.o)里同时出现时, LD报错. 提示符号多重定义.

当一个符号在多个静态库(.a)里同时出现时, LD不报错, 以第一个遇到的为准. 并且不会有任何warning提示 !!! 这个潜规则可能导致很多意想不到的问题!!!

测试代码:
helper.h



helper1.cpp



helper2.cpp


symbol_in_multi_obj.cpp


编译命令:


测试结果:
链接.o文件失败, 提示符号重定义


链接.a文件成功. 但其实链接的是第一个.a中的符号
先链helper1.a, 输出 “call myfun in helper1.cpp”


先链helper2.a, 输出 “call myfun in helper2.cpp”


真实案例:

Shasha同学的cgi类中调用http_request_helper类发请求, 发现程序行为总是和预期的不一致, 后来排查发现, 在Makefile中有另外的依赖库(.a)中也打包了http_request_helper类, 但是类的实现比较老, 导致把后面的新库覆盖!而GCC/LD对此没有任何提醒!!! Shit.


附上perryyang同事的补充:

补充一些:

GCC/LD

1.做符号解析时,会把找到的第一个定义的代码链接进来(已经找到了就不再考虑后续的)

2.做Object链接时(*.o文件),每一个目标文件要做reloc操作,找到的第一个定义优先处理,再遇到一个相同的定义,就报"multip-definition 错误",这个可以通过-Wl,'-z muldefs'来解决,

-z muldefs会让ld在遇到重复定义时候,只处理第一个定义。

在运行时刻,如果:

1.存在多个相同的动态库名,则根据ldconfig中配置的库查找路径,先找到哪个就用哪个。如果机器中存在不同版本的动态库,则可能会用上错误的库,而从我们的代码中是检查不出错误的,

只能优先做"ldconfig -p | grep 库名"的检查,干掉一个不用的库就可以。

2.如果多个不同的动态库,拥有相同的全局变量名,则最后加载的动态库中的全局变量会冲掉之前加载的全局变量,导致结果异常(程序正常工作)

下面这篇文章对链接处理的说明的比喻挺贴切的:

http://webpages.charter.net/ppluzhnikov/linker.html

和VC++对比

另外, 对比VC++的行为, 是不一样的, VC++在发现多个依赖库中有同名符号时, 会符号重定义, 而不是默认的选择一个. 同时, VC++还提供了当符号重定义时忽略指定的库的能力.



GCC/LD编译链接潜规则 (第二弹) : 为什么总链接失败? --LD对依赖库的输入顺序敏感.

7a92l3PJGnvaDHxidQbLNa+HgZlPfW4n/AsNVwuPzj8RAjoyZZE

为什么明明指定了依赖的库,却还总是链接失败???

潜规则: LD对依赖库的输入顺序敏感.
LD在链接生成目标文件时, 会从左到有扫描输入的依赖库, 但是, 当依赖库之间也有依赖关系时,必须将依赖别人的库放在 “被别人依赖的库的前面. 否则,会链接失败! 这点和vc++很不一样哦.

举例: 有三个库a.a, b.a, c.a; 其中b.a依赖了a.a和c.a, a.a和c.a相互不依赖, 而target依赖了a.a和b.a

g++ -o target target.cpp a.a b.a c.a // 成功,

g++ -o target target.cpp a.a c.a b.a // 失败, 因为c.a应该在b.a后面

为什么b.a依赖了a.a, 而a.a放在b.a的前面却能成功? 
因为target也依赖了a.a, 在链接b.a时,a.a的符号已经被链进来了.

测试代码:
a.h


b.h


c.h

a.cpp

b.cpp

c.cpp

ld_sequence.cpp


执行结果:


更多参考:


程序库为开发者带来了方便,但同时也是某些混乱的根源。我们来看看链接器(Unix平台)是如何解析(resolve)对程序库的引用的(参考[1],p556)。

在符号解析(symbol resolution)阶段,链接器按照所有目标文件和库文件出现在命令行中的顺序从左至右依次扫描它们,在此期间它要维护若干个集合:(1)集合E是将被合并到一起组成可执行文件的所有目标文件集合;(2)集合D是所有之前已被加入E的目标文件定义的符号集合;(3)集合U是未解析符号(unresolved symbols,即那些被E中目标文件引用过但在D中还不存在的符号)的集合。一开始,E、D、U都是空的。

(1): 对命令行中的每一个输入文件f,链接器确定它是目标文件还是库文件,如果它是目标文件,就把f加入到E,并把f中未解析的符号和已定义的符号分别加入到U、D集合中,然后处理下一个输入文件。

(2): 如果f是一个库文件,链接器会尝试把U中的所有未解析符号与f中各目标模块定义的符号进行匹配。如果某个目标模块m定义了一个U中的未解析符号,那么就把m加入到E中,并把m中未解析的符号和已定义的符号分别加入到U、D集合中。不断地对f中的所有目标模块重复这个过程直至到达一个不动点(fixed point),此时U和D不再变化。而那些未加入到E中的f里的目标模块就被简单地丢弃,链接器继续处理下一输入文件。

(3): 当扫描完所有输入文件时如果U非空或者有同名的符号被多次加入D,链接器报告错误信息并退出。否则,它把E中的所有目标文件合并在一起生成可执行文件。

上述规则针对的是Unix平台链接器,而VC(至少VC6.0)linker则有相当的不同: 它首先依次处理命令行中出现的所有目标文件,然后依照顺序不停地扫描所有的库文件,直至U为空或者某遍(从头到尾依次把所有的库文件扫描完称为一遍)扫描过程中U、D无任何变化时结束扫描,此刻再根据U是否为空以及是否有同名符号重复加入D来决定是出错退出还是生成可执行文件。很明显Unix链接器对输入文件在命令行中出现的顺序十分敏感,而VC的算法则可最大限度地减少文件顺序对链接的影响。作者不清楚Unix下新的开发工具是否已经改进了相应的做法,欢迎有实践经验的朋友补充这方面的信息(补充于2005年10月10日: 经试验,使用gcc 3.2.3的MinGW 3.1.0的链接器表现与参考[1]描述的一致)。

2012/09/16 号补充
根据上文的ldd的符号解析规则, 我们推导出如下实用规则:
不要在一个文件cpp里面定义多个无关的类, 这样做的好处:
由于符号链接是以.o为最小单位的(而不是.a), 如果类都存放在不同的cpp里面, 那么可以不会链接一些无关的.o, 减少文件大小. 有时候, 还能避免编译失败(因为,一旦链进来一个.o,那么须保证该.o里面的所有符号都必须有定义).

案例:
在实施朋友ID密钥项目时, 发现一些没有用到朋友ID密钥项目的工程也必须链接朋友ID密钥的库才能编译通过. 经排查是因为这些项目用到了base_wapper.a(该.a里面仅包含了一个base_wapper.o), 而base_wapper.cpp里面定义了很多个类,其中有一个朋友ID转换的包装类, 这导致, 所有依赖base_wapper.a的项目必须能解析base_wapper.cpp中出现的所有符号, 所以杯具. 解决方法就是把base_wapper.cpp 拆分为base_wapper.cpp和xy_convert.cpp. 虽然最终还是打包为一个base_wapper.a(base_wapper.o+xy_convert.o), 但对于使用方, 由于没有用到xy_convert.o, 不用链接朋友ID密钥库也不会报错.




gcc在寻找头文件时, 会按照一定的顺序在很多个目录挨个寻找, 一旦找到一个即停止寻找. 如果项目中存在多个同名的头文件, 则以第一个为准, 后面的直接忽略. 这个特性会导致很多编译不通过的问题!

GCC的寻找依赖的头文件顺序为: (这里已a.h为例)
1) 优先在使用了#include ”a.h” 的文件所在的目录寻找a.h
2) 在GCC通过 –I 指定的包含路径中从左到右找.

测试代码:
a.h

b.h


把a.h 分别往a1, a2两个目录放置一份.
把b.h 分别往当前目录和b1目录放置一份.
目录结构如下:


主文件 gcc_include.cpp


======================================
下面我们gcc找到同名文件的不同版本的效果.

1) 优先找到a1/a.h. 


2) 
优先找到a2/a.h


可以看到, 当指定不同的包含路径顺序时, gcc引用了不同a.h, 导致输出的结果不一样.

3) 优先找到当前目录下的b.h

PS: 
虽然-I./b1指定了目录, 但这里为什么没有找到 ./b1/b.h, 是因为 gcc_include.cpp文件中包含了 #include “b.h”, 所以, 优先在gcc_include.cpp所在的文件寻找.

4) 优先找到b/b.h (先把当前目录的b.h删除)
此时终于输出了 ./b1/b.h

真实案例:
代码中用到了md5库, md5库有两个头文件, md5c.h 和 MD5Int.h, 其中MD5Int.h内部包含了md5c.h. 

正常情况下, 需要md5库的代码, 只需要包含 MD5Int.h 即可. 但是. 实际中却编译报错.
经查, 发现
1)代码库中包含有多个md5的库:
1:./md5
2:./md5/inc/MD5Int.h
3:./md5/inc/md5_global.h
4:./md5/inc/md5c.h
5:./plib/base_class/extern_libs/tdev/include/md5.h
6:./plib/base_class/extern_libs/tdev/include/md5c.h
7:./plib/base_class/extern_libs/tbase/include/tbase_md5.h
8:./plib/application/qzdata/src/profile/md5c.c
9:./plib/application/app_manage/shortmsg_mail/share/include/md5c_new.h
10:./plib/application/path/group/include/md5_simple.h
11:./tdev/tlib/md5.c
12:./tdev/tlib/md5.h
13:./tdev/tlib/md5c.c
14:./tdev/tlib/md5c.h
2) 在没有加入编译依赖中没有明确的加入MD5库依赖时,查看编译时的引入库路径,居然已经有了上面的tdev (是被PLIB_BASE引入的)
g++ -o obj/open_tfs_bus.o -MF dep/open_tfs_bus.d -c -O -ggdb3 -pipe -Wall -MMD -MP -D_REENTRANT -D_GNU_SOURCE -pthread -DQZONE_PROJECT_ROOT="/usr/local/qzone_v3.0/" src/open_tfs_bus.cpp -Iinc -
.....
I/home/nemo/code/isd_qzoneappbase_proj/outerlib/store_cloud/32/include -I/home/nemo/code/isd_qzoneappbase_proj/outerlib/plib/outerlib/logapi//include/ -I/home/nemo/code/isd_qzoneappbase_proj/outerlib/plib/base_class/extern_libs/tdev/include
......
这个玩意给后面的编译失败留下了隐患.
3) 经查,tdev/include下的md5c.h 和 md5/inc/md5c.h 文件不一样!!!
更垃圾的是, md5/inc/MD5Inc.h 真正需要的md5c.h是tdev/include目录下的那个!!! 自己目录下的有很多符号都没有定义!!!
这里不得不吐槽下plib的维护者们, 这是在搞毛啊.
4) 所以, 如果想要编译过去, 必须引入md5/inc/MD5Int.h, 同时让其引入tdev/include目录下md5c.h, 而不是md5/inc/md5c.h
5) 偷鸡取巧的解决方法是:
a)
引入md5库的顺序如下: (让MD5放在后面)
PLIB_BASE
MD5
b) 
代码中引入头文件的方式如下: (将md5c.h单独提出来,并且放在MD5Inc.h的前面)
#include "md5c.h" // 必须放在MD5Int.h前面, 否则编译报错. 因为MD5Int.h会引用一个错误的版本.
#include "MD5Int.h"
这样就可以编译过去了! 这样可以gcc引用的md5c.h是位于tdev/include目录下的, 而MD5Inc.h是位于md5/inc目录下的

PS:为什么会这样呢? 根据上面的潜规则自己推倒下吧~




GCC/LD编译链接潜规则 (第四弹) : ld 会把所有通过-l指定的动态库全部链接进最终的目标程序中, 无论是否真的用到(导致生产环境加载失败)

961ci+6E0x90ErtT+mK9dwOv7CFr4pCKDoaRbj8j3Ib/KOBXIHo

潜规则: ld 会把所有通过-l指定的动态库全部链接进最终的目标程序中, 无论是否真的用到.这会导致额外链接进去一些无用的库.


经常发现这样的情况, 在开发环境编译好的so或exe, 放到环境之后, 会提示加载失败, 原因是缺少某些依赖的so. 最常见的是找不到MySQL.so. 因为开发环境有mysql.so, 而生产环境木有.

解决办法: 不要通过-l链接无用的so.

下面是当前目录的文件, 以及各个文件的内容.



下面是测试过程, 结果是, 虽然ld_so.cpp没有用到liba.so的任何东西, 却还是把liba.so的信息链进来了, 导致运行时加载so失败.


把当前目录设为动态库的搜索路径之后, OK了.



不指定a.so时, 照样能链接成功.




GCC/LD编译链接潜规则 (第五弹) : 为什么会有undified symbol?

04d0ugKnyvJtx1snIGoTteO5TVfXOayBPxCk5BMqr94zlCu6xFw
问题一:
你是否遇到过: 经常遇到加载一些so时, 提示undified symbol,导致加载失败, 可是makefile明明包含了指定的库呀, 怎么还会undified ?

问题二:
你是否还遇到过, 通过ldd -r 查看so, 发现输出包含有undified symbol, 但却仍能正常加载.

问题一的解决之道
我们要解决的是, 如何让so正确的找到symbol.
可能原因之一:
假设我们知道undified symbol的符号确定存在xxx.a里面.
虽然使用了Makefile里面加上了xxx.a的依赖, 但是, 一定要把xxx.a放在最后面. 因为有可能so依赖的其他的.a库也用到了xxx.a.
(PS:为什么要放在后面, 参考本系列的其他文章)

现实案例:
某次编译so时, 对编译结果so文件ldd -r, 发现有undified symbol, 但是明明链接了对应的open_l5库, 为何还提示未找到符号?
file bin/spp_appwork.so
bin/spp_appwork.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), not stripped

ldd -r bin/spp_appwork.so 2>&1 | grep -v "tbase\|Tencent\|write_log\|dl"
linux-gate.so.1 => (0xbfffe000)
libpthread.so.0 => /lib/libpthread.so.0 (0xb7e7e000)
libz.so.1 => /usr/lib/libz.so.1 (0xb7e6c000)
libcrypto.so.0.9.8 => /usr/lib/libcrypto.so.0.9.8 (0xb7d40000)
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0xb7c5f000)
libm.so.6 => /lib/libm.so.6 (0xb7c3a000)
libgcc_s.so.1 => /lib/libgcc_s.so.1 (0xb7c2f000)
libc.so.6 => /lib/libc.so.6 (0xb7b0f000)
/lib/ld-linux.so.2 (0x80000000)
undefined symbol: _ZN7COpenL54LoadEii (bin/spp_appwork.so)
undefined symbol: _ZN7COpenL56UpdateEi (bin/spp_appwork.so)

通过调整open_l5库的位置, 问题解决.

可能原因之二:
该符号本来是应该属于其所依赖的某个so的, 但是由于当前环境的so版本较低, 没有包含指定的符号.

该猜测通过如下方式验证

// 按照正常的流程编译ld_so, 可以正常运行
nemo@vm04_sles10:[ld_so]$ g++ -o ld_so ld_so.cpp -L. -la
nemo@vm04_sles10:[ld_so]$ ./ld_so 
lalala
call in funa()

// 找一个其他的so替换liba.so, 
nemo@vm04_sles10:[ld_so]$ cp liba.so liba.so.bk
nemo@vm04_sles10:[ld_so]$ cp d.so liba.so

// 启动时会发现undefined symbol
nemo@vm04_sles10:[ld_so]$ ./ld_so 
lalala // <========== 程序的前半部分代码执行了, 输出了lalala
./ld_so: symbol lookup error: ./ld_so: undefined symbol: _Z4funav // <========== 报错了.

// 通过ldd -r 查看, 也可以看出未定义的符号.
nemo@vm04_sles10:[ld_so]$ ldd -r ld_so
undefined symbol: _Z4funav (./ld_so) // <============= 未定义的符号.
linux-gate.so.1 => (0xbfffe000)
liba.so (0xb7f87000)
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0xb7e9d000)
libm.so.6 => /lib/libm.so.6 (0xb7e78000)
libgcc_s.so.1 => /lib/libgcc_s.so.1 (0xb7e6d000)
libc.so.6 => /lib/libc.so.6 (0xb7d4c000)
/lib/ld-linux.so.2 (0xb7f8a000)

现实案例二:
本人在制作绿色版lamp时也遇到类似问题. 在启动apache时, 提示加载libphp5.so失败: undefined symbol: xmlTextReaderSetup
经排查发现, 系统的/usr/local/bin目录存在一个libxml2.so, 和我在当时编译php时使用的libxml2的版本不一致, 通过修改 LD_LIBRARY_PATH, 使之优先寻找我指定目录下的的libxml2库解决.

问题二的解决之道
即使有undefined symbol:, 只要代码在执行的过程中用不到 也能正常加载so ? 
是的. 但是, 一旦在用到的时候, 就会报错了. 就比如上面的例子, 执行ld_so输出了lalala, 然后才报错未找到符号undefined symbol: _Z4funav.

说明: 
ld 在链接生成so的时候, 允许undefined symbol, 因为这些确实的符号可能在so被加载时就解决了. 
ld 在链接生成可执行文件的时候, 是不允许undefined symbol的

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值