我们在编写C++程序时经常用到.so库,有外部的也有自己编写的,那么在程序编译后,如何查看可执行程序或动态库的依赖关系呢?有些项目启动时加载大量的.so库导致启动速度慢,如何便捷的清理已不使用的so文件呢?本文就给大家简单讲一下。
一、ldd查看依赖
我们可以通过ldd指令来查看某个程序使用了那些动态库,这里我们看下linux系统的/bin/ls可执行程序都依赖了哪些库:
$ ldd /bin/ls
linux-vdso.so.1 (0x00007ffcb7d79000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f928cc5a000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f928ca00000)
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f928c969000)
/lib64/ld-linux-x86-64.so.2 (0x00007f928ccd1000)
这可以看到ls命令也依赖了不少so库,但如果你使用ldd对各种可执行程序或so查看依赖时却会发现,它列出的so库却不一定都是需要使用的,例如下边这个测试程序,我们通过ldd -u来查看下:
$ ldd -u output/bin/main
Unused direct dependencies:
/lib/x86_64-linux-gnu/libprotobuf-lite.so.23
/lib/x86_64-linux-gnu/libfastcdr.so.1
/lib/x86_64-linux-gnu/libtinyxml2.so.9
/lib/x86_64-linux-gnu/libssl.so.3
/lib/x86_64-linux-gnu/libcrypto.so.3
/usr/local/lib/librpc.so
/lib/x86_64-linux-gnu/libm.so.6
虽然没有用到,但是一样被依赖链接进来了,我们接下来看看程序启动时候有没有去加载它们。
二、strace跟踪加载
strace指令不但能跟踪程序IO,还能跟踪程序的所有系统调用,我们使用它来查看下程序启动过程中是否有加载这些没有用到的so库:
$ strace ./output/bin/main >> strace.log
$ grep "open" strace.log|grep ".so" |awk -F '"' '{print $2}'|sort|uniq -c |grep libprotobuf-lite
/lib/x86_64-linux-gnu/libprotobuf-lite.so.23
可以看到不用的库实际也被加载了,所以一定会影响进程的拉起速度。
原因可以看下linux程序拉起成为进程的三个基本步骤:
- 1.fork进程,在内核创建进程相关内核项,加载进程可执行文件;
- 2.查找依赖的so,分别加载并映射虚拟地址;
- 3.初始化程序变量;
第2步中so依赖越多,进程启动越慢,并且发布程序的时候,这些链接了但没有使用的so,同样要一起跟着发布,否则进程会因找不到so而启动失败。
三、Wl,–as-needed忽略未使用依赖
我们不能像上面那样,把一些毫无意义的so链接进来,浪费资源。但是开发人员写cmakefile一般有没有那么细心,只要能编译通过就行,那么有什么比较简单的方法来解决吗?有的,我们可以通过 -Wl,–as-needed 编译选项来自动忽略未使用的so库:
$ vim CMakeLists.txt
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wl,--as-needed")
或
$ g++ -Wl,--as-needed -o build/main main.cc
$ ldd -u output/bin/main # 结果为空
$ ldd output/bin/main # 少了未用库的依赖
四、readelf查看信息
ELF(Executable and Linking Format)是一个定义了目标文件内部信息如何组成和组织的文件格式。内核会根据这些信息加载可执行文件,内核根据这些信息可以知道从文件哪里获取代码,从哪里获取初始化数据,在哪里应该加载共享库等信息。可执行程序、.so共享库、.o目标文件等都属于elf文件。
我们可以通过readelf指令查看elf文件的头信息:
$readelf -h ./output/bin/main
ELF Header:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - GNU
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x17d30
Start of program headers: 64 (bytes into file)
Start of section headers: 1180968 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 41
Section header string table index: 40
header信息解读:
- 1.根据Class、Type和Machine,可以知道该文件在X86-64位机器上生成的64位可执行文件。
- 2.根据Entry point address,可以知道当该程序启动时从虚拟地址0x400420处开始运行。这个地址并不是main函数的地址,而是_start函数的地址,_start由链接器创建,_start是为了初始化程序。通过这个命令可以看到_start函数,objdump -d -j .text test
- 3.根据Number of program headers,可以知道该程序有8个段。
- 4.根据Number of section headers,可以知道该程序有30个区。区中存储的信息是用来链接使用的,主要包括:程序代码、程序数据(变量)、重定向信息等。比如:Code section保存的是代码,data section保存的是初始化或未初始化的数据,等等。
通过readelf指令查看elf文件的动态库依赖:
$ readelf -d ./output/bin/main
Dynamic section at offset 0x2d9c0 contains 38 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libasan.so.6]
0x0000000000000001 (NEEDED) Shared library: [libtest.so]
0x0000000000000001 (NEEDED) Shared library: [libprotobuf.so.23]
0x0000000000000001 (NEEDED) Shared library: [libfastrtps.so.2.8]
0x0000000000000001 (NEEDED) Shared library: [libjsoncpp.so.25]
0x0000000000000001 (NEEDED) Shared library: [libglog.so.1]
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000001d (RUNPATH) Library runpath: [/usr/local/lib:/home/test/cmake/build:xxx:]
五、so库的搜索路径
linux链接so的方式除了上边提到的编译依赖以外,还可以通过程序调用dlopen来打开相关so,这种方式不依赖直接的编译链接,ldd是看不到的。当进程运行到调用dlopen代码地方才加载该so,所以如果每个进程显示链接xxx.so,但是如果发布该程序时候忘记附带发布该.so,程序仍然能够正常启动,只要运行逻辑没有触发运行到调用dlopen函数代码地方,程序也不会报错。
不论是dlopen显示调用还是直接的编译依赖,在运行时都需要能找到对应的.so文件,默认情况下会缺省搜索路径/usr/lib、/lib目录,其他lib路径可以使用LD_LIBRARY_PATH环境变量来添加:
# 添加自定义.so库目录
export LD_LIBRARY_PATH=/home/work/test/lib:${LD_LIBRARY_PATH}
export只对当前.sh有效,我们可以把它添加到 ~/.bashrc中来对当前用户生效,也可以添加到/etc/bashrc中来对所有用户生效。
除了上述方法,还可以把lib目录添加到/etc/ld.so.conf文件中,保存过后ldconfig一下即可。(ldconfig 主要是在默认搜寻目录(/lib和/usr/lib)及动态库配置文件/etc/ld.so.conf内所列目录下搜索出可共享的动态链接库(格式如前介绍,lib*.so*),进而创建出动态装入程序(ld.so)所需的连接和缓存文件。缓存文件默认为/etc/ld.so.cache,此文件保存已排好序的动态链接库名字列表)
$ sudo vim /etc/ld.so.conf
......
/home/work/test/lib
$ ldconfig
六、objdump查看symbol符号表
有时我们在使用.so库的时候,会遇到undefined symbol的错误:
undefined symbol: _ZN7xxxxxxx9xxxxxxxxx20xxxxxxxxxxComponentC2Ev
这时我们可以通过以下方式查看:
objdump -T libxxx.so |grep _ZN7xxxxxxx9xxxxxxxxx20xxxxxxxxxxComponent
00000000002ec640 w DF .text 0000000000000922 Base _ZN7xxxxxxx9xxxxxxxxx20xxxxxxxxxxComponentD2Ev
这里可以看到.so的符号表里是有这个类的,只是要用的版本与.so里的版本有差异,一般是编译时依赖的.so与运行时的.so版本不一致导致的,改为使用一致的.so版本即可。
七、总结
好了,今天主要讲解了如何使用ldd/readelf查看so库依赖、使用strace跟踪加载、使用Wl,–as-needed忽略未使用依赖,以及运行时的.so搜索路径,大家还有什么想了解的可以给我留言,欢迎沟通。
23.5.10
参考:
http://blog.chinaunix.net/uid-27105712-id-3313293.html
https://www.cnblogs.com/lidabo/p/5705165.html