C++编译优化之—so动态库依赖

我们在编写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

欢迎关注下方“非著名资深码农“公众号进行交流~

发表评论

邮箱地址不会被公开。