静态库依赖静态库,有顺序的问题,否则undefined reference
至于动态链接,链接器会对依赖进行整理,避免这个问题。动态库就不存在依赖顺序的问题。
https://murphypei.github.io/blog/2019/06/link-sort
如果库是相互独立的,则顺序不重要。如果不是相互独立,那么必须对它们进行排序
对于日常命令行编译命令,一般从左到右分别是可执行文件 ——> 高级库 ——> 底层库,避免循环依赖;越是底层的库,越是往后面写,可以参考下述命令通式:
g++ ... obj($?) -l(上层逻辑lib) -l(中间封装lib) -l(基础lib) -l(系统lib) -o $@
- 静态库有顺序问题,并且要把自己的库所依赖的所有的第三方库都要显示的指定出来。
动态库无顺序问题,并且只需要显示的连接自己的动态库,自己的动态库依赖的第三方的动态库无需显示指定,自己会从rpath中自动连接第三方的动态库。
但必须把第三方库依赖的所有的库,包括路径都拷贝出来。
例如使用libevent.so, 此时需要把下面这一堆文件都拷贝出来:
lrwxrwxrwx. 1 root root 21 Mar 25 15:18 deps/so/libevent-2.1.so.7 -> libevent-2.1.so.7.0.1 -rwxr-xr-x. 1 root root 386024 Mar 25 15:18 deps/so/libevent-2.1.so.7.0.1 lrwxrwxrwx. 1 root root 26 Mar 25 15:17 deps/so/libevent_core-2.1.so.7 -> libevent_core-2.1.so.7.0.1 -rwxr-xr-x. 1 root root 241936 Mar 25 15:17 deps/so/libevent_core-2.1.so.7.0.1 lrwxrwxrwx. 1 root root 26 Mar 25 15:17 deps/so/libevent_core.so -> libevent_core-2.1.so.7.0.1 lrwxrwxrwx. 1 root root 21 Mar 25 15:18 deps/so/libevent.so -> libevent-2.1.so.7.0.1
- 把第三方静态库链接到自己的.so动态库,编译第三方静态库的时候需要加 -fPIC这个参数。
- 可以依赖第三方动态库生成自己的动态库
set(CMAKE_INSTALL_RPATH ${MY_RUNTIME_PATH})
add_library(my_dynamic SHARED SRC_list)
TARGET_LINK_LIBRARIES(my_dynamic ${other_so1_so2})
此时生成的libmy_dynamic.so动态库的rpath是${MY_RUNTIME_PATH},
别人利用我的libmy_dynamic.so动态库编译可执行程序时指定的rpath和编译libmy_dynamic.so动态库指定rpath不一致的时候,即使可执行程序指定的rpath路径下包含了所有的动态库,运行时还是报找不到动态库。
两种解决办法:
- 两处的rpath保持一致
- 通过export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}: new_path
所有的动态库都拷贝到new_path路径下。
https://blog.xizhibei.me/2019/02/24/why-library-order-matters-in-cpp-static-linking/
链接过程
我们在编译的过程中,有两种链接方式,动态与静态。
- 静态链接:即将依赖库与调用程序链接成一个完成的库或者可执行文件,运行的时候会将整个程序装到内存中,方便部署但是体积较大,依赖库升级的时候需要重新编译;
- 动态链接:即将依赖库与调用程序分离,不组装成单个文件,而是在运行的时候,当调用到动态库的库时,才会将依赖库装载到内存中,这样会方便与其它程序共享以及升级,可执行文件的体积小,但是不方便部署;
今天我们遇到的问题,发生在静态链接过程中,而链接过程的细节如下:
静态库中,包含着所有的 obj(*.o) 文件,连接器从左至右搜索,维护着一个 undefined 列表,一旦遇到没有定义的内容,就会将它加到列表中,如果搜索到了定义的内容,则抽取出 obj 文件,进行链接,并将 undefined 内容移出列表,
而其它 obj 文件就会被丢弃(为了减少最终的体积大小),于是一个静态库如果不能在搜索过程中被链接,它就会被丢弃,而在后面一旦遇到依赖它的库,就会造成引用无法被链接,一直留在undefined 列表中,最终导致编译错误。
- 静态库的环形链接:
然后我们再来个复杂点的:
$ cat a.cpp
extern int a;
int main() {
return a;
}
$ cat b.cpp
extern int b;
int a = b;
int d;
$ cat d.cpp
extern int d;
extern int e;
int b = d + e;
$ cat e.cpp
int e
再运行上面的编译后:
$ g++ -c e.cpp -o e.o
$ g++ -c b.cpp -o b.o
$ ar cr libb.a b.o e.o # 注意这里,把 b 与 e 在同一个静态库 libb.a 里面
$ g++ -c d.cpp -o d.o
$ ar cr libd.a d.o
会发现那两种方式都不能解决问题。
而下面这种方式却能解决
$ g++ a.cpp -L. -lb -ld -lb
为什么?
很简单,因为回顾下链接的过程就能发现,当链接器遇到第一个 lb 时,会将 b 加入 undefined 列表,而遇到 ld 时,会将 b 与 ld 链接,同时将 d 与 e 加入 undefined 列表,最后遇到 第二个 lb 时,重复同样的过程,然后顺利链接。
但是,反过来:
$ g++ a.cpp -L. -ld -lb -ld
却会失败。
https://blog.csdn.net/sweetfather/article/details/92691637
- 链接的库文件中又使用了另一个库文件
这种问题比较隐蔽,也是我最近遇到的与网上大家讨论的不同的问题,举例说明如下,首先,还是看看测试代码。
从上图可以看出,main.c调用了test.c的函数,test.c中又调用了fun.c的函数。 首先,我们先对fun.c,test.c,main.c进行编译,生成 .o文件。
- gcc -c func.c
- gcc -c test.c
- gcc -c main.c
然后,将test.c和func.c各自打包成为静态库文件。
- ar –rc func.a func.o
- ar –rc test.a test.o
这时,我们准备将main.o链接为可执行程序,由于我们的main.c中包含了对test()的调用,因此,应该在链接时将test.a作为我们的库文件,链接命令如下。
- gcc -o main main.o test.a
这时,编译器仍然会报错,如下:
- test.a(test.o): In function `test':
- test.c:(.text+0x13): undefined reference to `func'
- collect2: ld returned 1 exit status
就是说,链接的时候,发现我们的test.a调用了func()函数,找不到对应的实现。由此我们发现,原来我们还需要将test.a所引用到的库文件也加进来才能成功链接,因此命令如下。
- gcc -o main main.o test.a func.a
ok,这样就可以成功得到最终的程序了。同样,如果我们的库或者程序中引用了第三方库(如pthread.a)则同样在链接的时候需要给出第三方库的路径和库文件,否则就会得到undefined reference的错误。
- 4 多个库文件链接顺序问题
这种问题也非常的隐蔽,不仔细研究你可能会感到非常地莫名其妙。我们依然回到第3小节所讨论的问题中,在最后,如果我们把链接的库的顺序换一下,看看会发生什么结果?
- gcc -o main main.o func.a test.a
我们会得到如下报错.
- test.a(test.o): In function `test':
- test.c:(.text+0x13): undefined reference to `func'
- collect2: ld returned 1 exit status
因此,我们需要注意,在链接命令中给出所依赖的库时,需要注意库之间的依赖顺序,依赖其他库的库一定要放到被依赖库的前面,这样才能真正避免undefined reference的错误,完成编译链接。
https://blog.csdn.net/kl1125290220/article/details/103081969
https://blog.csdn.net/yuhengyue/article/details/83542240
假设程序test依赖动态库b,而动态库b依赖动态库a。
在编译test的时候,我们希望的是只指定b,而不用指定a,因为我们不希望知道a的依赖库有哪些,只需关心b。那么我们采用这样的思路去编译test的时候,是会报错的。比如:
我们编译动态库a:gcc a.c -o liba.so -shared -fPIC
我们编译动态库b:gcc b.c -o libb.so -shared -fPIC -I../a/ -L../a/ -la
我们编译test:gcc main.c -o test -I../b -L../b -lb
程序会报错如下:
/usr/bin/ld: warning: liba.so, needed by ../b/libb.so, not found (try using -rpath or -rpath-link)
../b/libb.so: undefined reference to `a'
collect2: ld returned 1 exit status
错误的原因在于程序没能自动找到liba.so,因为liba.so不在ld的默认搜索路径里。解决方法是如下编译:
gcc main.c -o test -I../b -L../b -lb -Wl,-rpath=../a
【不显示指定第三方被依赖的动态库也行,只要指定了其rpath路径就可以,ldd libb.so可以看到依赖了liba.so】
————————————————
libB.so的源码:
#include <stdio.h>
int funB1(){
printf("in funB1");
return 0;
}
int funB2(){
printf("in funB2");
return 0;
}
这里面有两个函数:funB1和funB2。
其中funB1函数会被libA调用,而funB2会被可执行文件调用。
编译libB.so:
$ gcc libB.cpp -fPIC -shared -o libB.so
1
libA.so的源码:
#include <stdio.h>
int funB1();
int funA1(){
printf("in funA1 \n");
funB1();
return 0;
}
该库中只有一个函数funA1,该函数在内部调用了libB中的funB1函数。且该函数会被可执行文件调用。
编译libA.so:
$ gcc libA.cpp -fPIC -shared -o libA.so -Wl,-rpath=./ -L./ -lB
1
main.cpp的源码:
int funA1();
int funB2();
int main(){
funA1();
funB2();
return 0;
}
编译main.cpp:(复现错误的编译方法)
gcc main.cpp -L./ -lA
1
当我们按照上面的指令编译main.cpp的时候,便报错了。
/usr/bin/ld: /tmp/ccDQXTKy.o: undefined reference to symbol '_Z5funB2v'
.//libB.so: error adding symbols: DSO missing from command line
collect2: error: ld returned 1 exit status
(问号.jpg)这,这GCC不是搞事吗,你明明知道我需要连接libB.so为啥就不帮我链接上去呢?难道我libA.so没有指明要使用libB.so?我们使用下面的指令来看一下
$ ldd libA.so
1
得到如下信息:
linux-vdso.so.1 => (0x00007ffd09def000)
libB.so => ./libB.so (0x00007fc513d7d000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc5139b3000)
/lib64/ld-linux-x86-64.so.2 (0x00007fc514181000)
明明libA.so已经显式的指明我要依赖libB.so了,那为啥在编译main.cpp的时候链接了libA.so,GCC却还要我们显式的链接libB.so呢?
3 答案
答案很简单,那就是GCC就是想要你显式链接呗。(你是编译器,你牛好吧。)那这是为啥呢?
官方一点的答案就是,自从binutils 2.22版本以后,如果你在程序中使用了你依赖的动态库所依赖的动态库中的函数时,你就必须显式的指定你依赖的动态库所依赖的动态库。
说那么多,我们更想知道的是,通过修改什么参数可以解决这个问题呢?因为你可能不想在编译程序的时候要把动态库所依赖的所有动态库都显示链接一遍。
4 究极答案
实际上,这是binutils在2.22版本以后,默认把--no-copy-dt-needed-entries这个选项打开了。当打开了这个选项的时候,编译器在链接的时候是不会递归的去获取依赖动态库的依赖项的,于是就会出现上述的问题。关于该配置项的详细说明如下:
--copy-dt-needed-entries
--no-copy-dt-needed-entries
This option affects the treatment of dynamic libraries referred to by DT_NEEDED tags inside ELF dynamic libraries mentioned on the command line. Normally the linker won't add a DT_NEEDED
tag to the output binary for each library mentioned in a DT_NEEDED tag in an input dynamic library. With --copy-dt-needed-entries specified on the command line however any dynamic
libraries that follow it will have their DT_NEEDED entries added. The default behaviour can be restored with --no-copy-dt-needed-entries.
This option also has an effect on the resolution of symbols in dynamic libraries. With --copy-dt-needed-entries dynamic libraries mentioned on the command line will be recursively
searched, following their DT_NEEDED tags to other libraries, in order to resolve symbols required by the output binary. With the default setting however the searching of dynamic
libraries that follow it will stop with the dynamic library itself. No DT_NEEDED links will be traversed to resolve symbols.
大概意思就是,跟在--no-copy-dt-needed-entries它后面的库都不会遍历其依赖项,使用--copy-dt-needed-entries则相反。也就是使用下面的指令来编译mian.cpp就可以避免该问题了。
$ gcc main.cpp -L./ -Wl,--copy-dt-needed-entries -lA
1
题外话
在Linux的ELF文件中,如果依赖于其他的动态库,那么改ELF文件会存在一个.dynamic的段,这个段里面会记录其依赖的动态库信息,其标志位为DT_NEEDED。
最近为公司开发的开源超级轻量级插件系统(https://github.com/HSRobot/HPlugin),欢迎大家交流讨论。
5 参考文档
1,DSO missing from command line原因及解决办法:https://segmentfault.com/a/1190000002462705
2,折腾gcc/g++链接时.o文件及库的顺序问题: https://www.cnblogs.com/OCaml/archive/2012/06/18/2554086.html#sec-1-4-1
————————————————