Linux动态链接库以及链接器相关知识

网上关于Linux中动态链接库和动态链接库的博客有很多,其实大部分博客都没有分清楚linux下面动态链接库运行时的查找路径跟编译期的查找路径并不相同,导致很多人在编译程序时库引用出错,按教程添加库路径后依然出错。下面一并总结,不涉及太多二进制中ELF的相关内容。
Linux系统中有大量共用的动态链接库和静态链接库,当对程序源代码编译之后进行链接时往往需要引用系统中已经有的动态链接库或静态链接库。关于动态链接库可以简单理解为在链接时只是将动态库中需要引用的内容(包括变量、函数)在目标程序的符号表中创建一个链接,只在运行到该部分调用时才去查看动态链接库,linux下动态链接库通常为libxxx.so,windows下为.dll。而静态链接库是将整个库一并打包到链接后的目标程序中,并直接映射好所需要的符号表,静态链接库后缀通常为.a,windows下通常为.lib。如果也不明白符号表,可以参考实例验证C/C++源代码变成程序的过程

Linux下有两个很好用的实用程序:

  • ldd:用于查看共享库依赖信息
  • ldconfig:用于配置动态链接器的运行时绑定

还有两个Linux下非常重要的动态链接器/载入器:

  • ld.so
  • ld-linux.so*

以及GNU的链接器:

  • ld:这个就是将一堆的目标文件通过符号表链接成最终的程序,可以参考上面推荐的另一篇文章

ldd

通过ldd命令后面直接跟文件名即可,不仅可以查看可执行二进制程序的依赖库,也可以查看库本身的依赖,如:

1
ldd /lib/x86_64-linux-gnu/libm.so.6

输出为:

1
2
3
linux-vdso.so.1 =>  (0x00007ffd3afe6000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f35df5f5000)
/lib64/ld-linux-x86-64.so.2 (0x000055a75e17f000)

查看ls的依赖库:

1
ldd /bin/ls

输出为:

1
2
3
4
5
6
7
linux-vdso.so.1 =>  (0x00007ffc6492b000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f5128ace000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5128705000)
libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f5128494000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f5128290000)
/lib64/ld-linux-x86-64.so.2 (0x0000563eda70b000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f5128073000)

对于运行时或链接时可能会出现的undefined symbol错误可以通过ldd -r <libxx.so>来查看其有哪些符号未定义,它会输出很详细的相关符号的名字,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ ldd -r /usr/lib/libGL.so
linux-gate.so.1 => (0xf77a5000)
libdl.so.2 (0xf771c000)
libGLX.so.0 (0xf76fd000)
libGLdispatch.so.0 (0xf767d000)
libc.so.6 (0xf74bc000)
/lib/ld-linux.so.2 (0xf77a6000)
libX11.so.6 => not found
libXext.so.6 => not found
undefined symbol: _XAsyncErrorHandler (libGLX.so.0)
undefined symbol: _XFlush (libGLX.so.0)
undefined symbol: XScreenCount (libGLX.so.0)
undefined symbol: _XReply (libGLX.so.0)
undefined symbol: XFree (libGLX.so.0)
undefined symbol: _XDeqAsyncHandler (libGLX.so.0)
undefined symbol: _XError (libGLX.so.0)
undefined symbol: XQueryExtension (libGLX.so.0)
undefined symbol: _XRead (libGLX.so.0)
undefined symbol: XESetCloseDisplay (libGLX.so.0)
undefined symbol: XAddExtension (libGLX.so.0)
undefined symbol: _XEatData (libGLX.so.0)

通过这个名字可以知道这个是为了支持C++重载被编译器mangle之后的函数引用名字,如果是C语言引用C++的库中的函数,此处的库函数引用应该加上extern "C".

也可以使用nm -A <libxxx.so>来查看符号表

更多ldd功能查看man ldd

ldconfig

ldconfig管理的库路径通常有/usr/lib/lib/usr/lib64/lib64以及/etc/ld.so.conf中指定的所有路径。
ldconfig常用的功能如下:

更新动态链接库缓存

1
sudo ldconfig

当安装完新库或者在/etc/ld.so.conf,或者/etc/ld.so.conf.d/*.conf中添加库路径时都需要运行ldconfig来更新缓存使新添加的库生效。当然系统启动时会自动运行ldconfig,因为好多软件重装完成之后都会建议重启电脑。

查看ld搜索到的所有库路径以及创建的库链接

1
sudo ldconfig -v

-v参数也就是--verbose模式,很多人称为开启啰嗦模式也很贴切

只输入动态链接库搜索路径

1
sudo ldconfig -v 2>/dev/null | grep -v ^$'\t'

只查看当前缓存中的库

1
sudo ldconfig -p

链接器

程序运行时库查找路径

ld.sold-linux.so可以在程序运行前搜索所需要支持库路径,它们的库搜索路径除了在ldconfig中提到的之外,还包含通过环境变量LD_LIBRARY_PATH指定的路径。
所以当你的程序在运行时提示缺少库,可以通过将所需要的的动态库添加到该环境变量中,或者加入到/etc/ld.so.conf中,然后采用ldconfig更新库缓存。
更多内容可以man ld.so

编译期链接库路径

当通过GNU的gcc或g++编译并链接程序时(下面举例中使用的gcc,g++情况类似),其实链接部分的功能跟刚才提到的链接器ld一样,甚至很多时候就是调用系统中的ld。在将目标文件链接成可执行的二进制程序时搜索的库路径通常都是系统库路径,注意与程序运行时载入的动态库路径不一样,所以无论你怎么修改运行时搜索库目录,编译依然会失败,可以通过以下方式查看链接器ld的库搜索路径:

1
ld --verbose | grep SEARCH_DIR | tr -s ' ;' \\012

或者通过gcc的啰嗦模式查看,gcc会比ld多搜索通过-L参数指定的库路径,以及环境变量LIBRARY_PATH中指定的路径:

1
gcc -print-search-dirs | sed '/^lib/b 1;d;:1;s,/[^/.][^/]*/\.\./,/,;t 1;s,:[^=]*=,:;,;s,;,;  ,g' | tr \; \\012

所以当你在编译并链接程序时如果出现库缺少的情况,可以通过-L来指定所需要的库,gcc的-L用法为-Ldir,或者将其加到环境变量LIBRARY_PATH中,注意前面没有LD_。静态库可以通过-llibrary-l library指定。
注意这里跟通过-I参数指定头文件搜索路径不同,gcc的-Idir用来指定头文件搜索路径。当然也可以通过以下环境变量来为gcc或g++添加头文件搜索路径:

1
2
export CPLUS_INCLUDE_PATH=/usr/lib/jvm/java-8-oracle/include/:/usr/lib/jvm/java-8-oracle/include/linux/:$CPLUS_INCLUDE_PATH
export C_INCLUDE_PATH=/usr/lib/jvm/java-8-oracle/include/:/usr/lib/jvm/java-8-oracle/include/linux/:$C_INCLUDE_PATH

上面那行用于g++,下面那行用于gcc,本例是添加java的jni头文件路径

关于LD_LIBRARY_PATHLD_RUN_PATH的区别,下面这两句来自gcc的make install提示很棒

  • add LIBDIR to the LD_LIBRARY_PATH environment variable during execution
  • add LIBDIR to the LD_RUN_PATH environment variable during linking

更多内容可以man ldman gcc

gcc编译时verbose选项

使用gcc编译时可以通过添加-v或者--verbose来开启啰嗦模式,查看链接器的调用情况,但并不会显示链接器的啰嗦模式的输出内容。

可以通过-Wl向链接器传递参数的方式来查看链接器的啰嗦模式信息输出情况,例如-Wl,--verbose,gcc的-Wl的用法为-Wl,option,其中option为要传递给链接器的参数。

更多内容可以man gcc

参考:
1.How to print the ld(linker) search path

2.LD_LIBRARY_PATH vs LIBRARY_PATH
当然还有一堆的man手册