Last Updated: 2023-07-24 09:57:31 Monday
-- TOC --
当你在Linux中运行一个程序,系统提示找不到某个动态链接库,或者某个符号在库中找不到,这可能是我们常有的经历。比较痛苦的是,如果没有思路,这个问题有时还很不好解决,有时甚至会导致系统重装。这篇笔记,尝试总结我个人在动态链接库方面的积累。
可以这么说,动态链接已经是软件系统中必不可少的一个重要系统机制。它带来的好处,远远大于它引入的影响,几乎所有OS都支持动态链接机制。gcc编译器默认就采用动态链接,除非使用-static
参数。
动态链接带来的好处:
计算机系统包含一些长期发展迭代很多次后沉淀下来的几乎不再变化的设计思想和数学原理,这些可以看做是计算机系统的
基石设计
,动态链接应该可以算一个。
下面这小段代码,什么都不干,动态链接和静态链接的输出size差异巨大:
int main() {
return 9;
}
动态链接和静态链接的输出size:
$ gcc test.c -o test
$ ls -l test
-rwxr-xr-x. 1 xinlin xinlin 24752 Jul 7 09:22 test
$ gcc -static test.c -o test
$ ls -l test
-rwxr-xr-x. 1 xinlin xinlin 1928000 Jul 7 09:22 test
$ echo $((1928000/24752))
77
静态链接后,程序文件size暴增77倍!
程序代码在(动态)链接的时候,如果某个外部符号属于动态链接库,此时链接器无法确定符号地址,因而推迟到加载的时候再确定。ELF文件中通过.got
中转动态链接库中的全局变量,通过.got.plt
中转动态链接库中的函数接口。
OS在启动某个进程时,首先判断此进程是否是动态链接的。如果是,先将控制权交给动态链接器,由动态链接器加载(映射)程序所需要的所有动态链接库,这个过程会涉及某些全局符号的重定位。当所有动态链接库在进程的虚拟地址空间中准备就绪,动态链接器再将控制权交给程序入口,此时程序才开始执行。
如下两篇笔记,更深入细致的分析了上述流程:
程序代码经过编译链接之后,动态链接器的位置就已经存放在了ELF文件的名叫.interp的section中了。查看程序的动态链接器:
$ gcc test.c -o test # dynamic link
$ readelf -p .interp test
String dump of section '.interp':
[ 0] /lib64/ld-linux-x86-64.so.2
同一系统下的几乎所有程序,都用这个相同的动态链接器/lib64/ld-linux-x86-64.so.2
。它的重要性不言而喻,没有它,所有程序都无法启动。
$ /lib64/ld-linux-x86-64.so.2 --version
ld.so (GNU libc) stable release version 2.35.
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
$ ldd /lib64/ld-linux-x86-64.so.2
statically linked
$ file /lib64/ld-linux-x86-64.so.2
/lib64/ld-linux-x86-64.so.2: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), static-pie linked, BuildID[sha1]=321b87738a8ee7220b3c045c0d4d93150be54470, with debug_info, not stripped
动态链接器本身是一个可以直接运行的静态链接的PIE,它是个特殊的存在,因为没有其它程序能够为它加载器依赖的动态链接库,它只能自己干。
有好几种方法,可以查看程序运行依赖的动态链接库。
ldd命令其实是一个shell脚本,行数不多,值得一读。
$ ldd /usr/bin/python
linux-vdso.so.1 (0x00007ffc5a43c000)
libpython3.10.so.1.0 => /lib64/libpython3.10.so.1.0 (0x00007f07c9c00000)
libc.so.6 => /lib64/libc.so.6 (0x00007f07c9800000)
libm.so.6 => /lib64/libm.so.6 (0x00007f07c9f6a000)
/lib64/ld-linux-x86-64.so.2 (0x00007f07ca061000)
$ ldd /lib64/libpython3.10.so.1.0
linux-vdso.so.1 (0x00007ffd8adfb000)
libm.so.6 => /lib64/libm.so.6 (0x00007f88a9522000)
libc.so.6 => /lib64/libc.so.6 (0x00007f88a9200000)
/lib64/ld-linux-x86-64.so.2 (0x00007f88a998e000)
动态链接库本身也可能会依赖其它动态链接库,一样可以用ldd命令查看。linux-vdso.so.1是个特殊的库。
readelf命令可以查看程序所依赖的动态链接库,但是依赖的依赖不能自动查询出来。
$ readelf -d /usr/bin/ls
Dynamic section at offset 0x209f8 contains 29 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libselinux.so.1]
0x0000000000000001 (NEEDED) Shared library: [libcap.so.2]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x4000
0x000000000000000d (FINI) 0x17d74
0x0000000000000019 (INIT_ARRAY) 0x20ef0
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x20ef8
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x458
0x0000000000000005 (STRTAB) 0x1080
0x0000000000000006 (SYMTAB) 0x498
0x000000000000000a (STRSZ) 1510 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x21c08
0x0000000000000002 (PLTRELSZ) 2568 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x2d50
0x0000000000000007 (RELA) 0x1838
0x0000000000000008 (RELASZ) 5400 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000000000001e (FLAGS) BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW PIE
0x000000006ffffffe (VERNEED) 0x1768
0x000000006fffffff (VERNEEDNUM) 2
0x000000006ffffff0 (VERSYM) 0x1666
0x000000006ffffff9 (RELACOUNT) 211
0x0000000000000000 (NULL) 0x0
NEEDED行显示出来此程序所依赖的动态链接库。如果用ldd查看这个程序,会发现多出一个库文件,这个多出来的库文件,就是libselinux.so.1的依赖。
当LD_TRACE_LOADED_OBJECTS
环境变量不为空时,运行程序会自动打印出其所依赖的所有动态链接库,程序本身不会被执行,只是打印这个信息。其实,这些信息就是动态链接器打印出来的,当此环境变量不为空时,动态链接器遍历出程序所有的库之后,不会将控制权交给程序入口,而是直接退出了。
$ LD_TRACE_LOADED_OBJECTS=1 ls
linux-vdso.so.1 (0x00007fff503b5000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f60d461a000)
libcap.so.2 => /lib64/libcap.so.2 (0x00007f60d4610000)
libc.so.6 => /lib64/libc.so.6 (0x00007f60d4400000)
libpcre2-8.so.0 => /lib64/libpcre2-8.so.0 (0x00007f60d4363000)
/lib64/ld-linux-x86-64.so.2 (0x00007f60d465a000)
前面介绍过,动态链接器是可以直接运行的一个程序,其也具有一些参数。其中,--list
参数就是用来显示所有依赖的库文件。这个机制是前面所有机制的最底层,除了readelf命令。
$ /lib64/ld-linux-x86-64.so.2 --list /usr/bin/ls
linux-vdso.so.1 (0x00007fffda9a4000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f12c1fdb000)
libcap.so.2 => /lib64/libcap.so.2 (0x00007f12c1fd1000)
libc.so.6 => /lib64/libc.so.6 (0x00007f12c1c00000)
libpcre2-8.so.0 => /lib64/libpcre2-8.so.0 (0x00007f12c1f34000)
/lib64/ld-linux-x86-64.so.2 (0x00007f12c203f000)
程序是静态概念,而程序运行起来之后,就是进程,进程是动态的概念。程序依赖的动态链接库,不一定与其运行起来的进程所加载的动态链接库完全对应,有可能程序会在运行时,使用explicit runtime linking技术加载一些库到进程空间,比如调用dlopen
接口。
理解explicit runtime linking,想一想Python的import一个C语言写的库,用的就是这个技术。
ldd是shell脚本,pldd是可执行程序。
启动一个interactive python,然后用pldd看一下:
$ pldd 4323 # 4323 is pid
4323: /usr/bin/python3.10
linux-vdso.so.1
/lib64/libpython3.10.so.1.0
/lib64/libc.so.6
/lib64/libm.so.6
/lib64/ld-linux-x86-64.so.2
/usr/lib64/python3.10/lib-dynload/readline.cpython-310-x86_64-linux-gnu.so
/lib64/libreadline.so.8
/lib64/libtinfo.so.6
/usr/lib64/python3.10/lib-dynload/_opcode.cpython-310-x86_64-linux-gnu.so
在Python Shell中执行import socket
,然后再用pldd查看:
$ pldd 4323
4323: /usr/bin/python3.10
linux-vdso.so.1
/lib64/libpython3.10.so.1.0
/lib64/libc.so.6
/lib64/libm.so.6
/lib64/ld-linux-x86-64.so.2
/usr/lib64/python3.10/lib-dynload/readline.cpython-310-x86_64-linux-gnu.so
/lib64/libreadline.so.8
/lib64/libtinfo.so.6
/usr/lib64/python3.10/lib-dynload/_opcode.cpython-310-x86_64-linux-gnu.so
/usr/lib64/python3.10/lib-dynload/_socket.cpython-310-x86_64-linux-gnu.so
/usr/lib64/python3.10/lib-dynload/math.cpython-310-x86_64-linux-gnu.so
/usr/lib64/python3.10/lib-dynload/select.cpython-310-x86_64-linux-gnu.so
/usr/lib64/python3.10/lib-dynload/array.cpython-310-x86_64-linux-gnu.so
import socket后,一大堆动态链接库就被加载了!
有可能pldd命令就是用这个方法,只是它做了去重。
这个命令直接查看进程的虚拟地址空间的映射,所有加载的动态链接库,都有映射。只是不同的segment会将库文件进行重复显示。
关于如何解读cat /proc/pid/maps
命令的输出,请参考:环境变量和进程地址空间分布。
见下。
见下。
有新库加入系统,或者有新的路径添加到ld.so.conf
,记得运行ldconfig命令。
以上两个子目录,以及本目录,都请参考:动态链接库的名称和路径(ldconfig命令)
rpath参数在ELF文件的.dynamic字段内,gcc编译的时候,可以传入这个参数。rpath参数的作用,增加动态链接器的搜索路径。
elf.h
文件中对rpath参数字段有一个deprecated注释,不建议使用,因为很容易造成在某台电脑上编译链接的动态链接库,copy到另一台电脑后,就无法正常运行的情况,两台电脑的相关路径很容易不一致。只有trusted directories才是很固定的。
做个测试:
先准备一个动态链接库:
$ cat t.c
#include <stdio.h>
void f(){
printf("i am in t.c\n");
}
$ gcc -fPIC -shared t.c -o libt.so
然后写一段测试代码:
void f();
int main() {
f();
return 0;
}
$ gcc test.c -o test -Xlinker -rpath ./ -L./ -lt
$ ./test
i am in t.c
查看test内的rpath参数:
$ readelf -d test | grep RUNPATH
0x000000000000001d (RUNPATH) Library runpath: [./]
如果不加-Xlinker -rpath ./
,test运行时报错:
$ gcc test.c -o test -L./ -lt
$ ./test
./test: error while loading shared libraries: libt.so: cannot open shared object file: No such file or directory
因此,程序运行时找不到动态链接库,虽然这个库文件就在当前路径,但当前路径不在搜范围中。解决此问题,最好是在编译链接时,不要设置rpath参数,而是使用LD_LIBRARY_PATH,如下:
$ LD_LIBRARY_PATH=./ ./test
i am in t.c
介绍一个chrpath
小工具,它可以直接修改ELF文件中的rpath参数,这个小工具要单独安装,一般系统内都没有。
$ sudo dnf install chrpath
这个小工具也能够直接将ELF文件中的rpath参数删除。
通过给这个环境变量设置不同的值,可以实现各种动态链接过程的调试功能。
使用前面的测试代码。当设置LD_DEBUG=files
时,可以打印出加载动态链接库的细节:
$ LD_DEBUG=files ./test
55925:
55925: file=libt.so [0]; needed by ./test [0]
55925: file=libt.so [0]; generating link map
55925: dynamic: 0x00007f7507b77e20 base: 0x00007f7507b74000 size: 0x0000000000004030
55925: entry: 0x00007f7507b74000 phdr: 0x00007f7507b74040 phnum: 11
55925:
55925:
55925: file=libc.so.6 [0]; needed by ./test [0]
55925: file=libc.so.6 [0]; generating link map
55925: dynamic: 0x00007f75079f7ba0 base: 0x00007f7507800000 size: 0x0000000000201d90
55925: entry: 0x00007f75078296d0 phdr: 0x00007f7507800040 phnum: 14
55925:
55925:
55925: calling init: /lib64/ld-linux-x86-64.so.2
55925:
55925:
55925: calling init: /lib64/libc.so.6
55925:
55925:
55925: calling init: ./libt.so
55925:
55925:
55925: initialize program: ./test
55925:
55925:
55925: transferring control: ./test
55925:
i am in t.c
55925:
55925: calling fini: ./test [0]
55925:
55925:
55925: calling fini: ./libt.so [0]
55925:
LD_DEBUG的其它有效值,可以通过help显示出来:
$ LD_DEBUG=help ./test
Valid options for the LD_DEBUG environment variable are:
libs display library search paths
reloc display relocation processing
files display progress for input file
symbols display symbol table processing
bindings display information about symbol binding
versions display version dependencies
scopes display scope information
all all previous options combined
statistics display relocation statistics
unused determined unused DSOs
help display this help message and exit
To direct the debugging output into a file instead of standard output
a filename can be specified using the LD_DEBUG_OUTPUT environment variable.
LD_DEBUG=libs
很有用,它可以打印出动态库的搜索路径,方便定位找不到库的问题!
通过上文介绍的LD_DEBUG环境变量,我们可以测试一下找不到动态链接库文件时的搜索路径:
$ LD_DEBUG=libs ./test
56235: find library=libt.so [0]; searching
56235: search cache=/etc/ld.so.cache
56235: search path=/lib64/glibc-hwcaps/x86-64-v2:/lib64/tls/x86_64/x86_64:/lib64/tls/x86_64:/lib64/tls/x86_64:/lib64/tls:/lib64/x86_64/x86_64:/lib64/x86_64:/lib64/x86_64:/lib64:/usr/lib64/glibc-hwcaps/x86-64-v2:/usr/lib64/tls/x86_64/x86_64:/usr/lib64/tls/x86_64:/usr/lib64/tls/x86_64:/usr/lib64/tls:/usr/lib64/x86_64/x86_64:/usr/lib64/x86_64:/usr/lib64/x86_64:/usr/lib64 (system search path)
56235: trying file=/lib64/glibc-hwcaps/x86-64-v2/libt.so
56235: trying file=/lib64/tls/x86_64/x86_64/libt.so
56235: trying file=/lib64/tls/x86_64/libt.so
56235: trying file=/lib64/tls/x86_64/libt.so
56235: trying file=/lib64/tls/libt.so
56235: trying file=/lib64/x86_64/x86_64/libt.so
56235: trying file=/lib64/x86_64/libt.so
56235: trying file=/lib64/x86_64/libt.so
56235: trying file=/lib64/libt.so
56235: trying file=/usr/lib64/glibc-hwcaps/x86-64-v2/libt.so
56235: trying file=/usr/lib64/tls/x86_64/x86_64/libt.so
56235: trying file=/usr/lib64/tls/x86_64/libt.so
56235: trying file=/usr/lib64/tls/x86_64/libt.so
56235: trying file=/usr/lib64/tls/libt.so
56235: trying file=/usr/lib64/x86_64/x86_64/libt.so
56235: trying file=/usr/lib64/x86_64/libt.so
56235: trying file=/usr/lib64/x86_64/libt.so
56235: trying file=/usr/lib64/libt.so
56235:
./test: error while loading shared libraries: libt.so: cannot open shared object file: No such file or directory
可以清晰的看到,搜索的路径全在/etc/ld.so.cache
中!
如果增加rpath参数,让它指向与可执行文件相同的路径,再看看动态链接的搜索路径:
$ gcc test.c -o test -Xlinker -rpath=./ -L./ -lt
$ LD_DEBUG=libs ./test
56327: find library=libt.so [0]; searching
56327: search path=./glibc-hwcaps/x86-64-v2:./tls/x86_64/x86_64:./tls/x86_64:./tls/x86_64:./tls:./x86_64/x86_64:./x86_64:./x86_64:. (RUNPATH from file ./test)
56327: trying file=./glibc-hwcaps/x86-64-v2/libt.so
56327: trying file=./tls/x86_64/x86_64/libt.so
56327: trying file=./tls/x86_64/libt.so
56327: trying file=./tls/x86_64/libt.so
56327: trying file=./tls/libt.so
56327: trying file=./x86_64/x86_64/libt.so
56327: trying file=./x86_64/libt.so
56327: trying file=./x86_64/libt.so
56327: trying file=./libt.so
56327:
56327: find library=libc.so.6 [0]; searching
56327: search path=./glibc-hwcaps/x86-64-v2:./tls/x86_64/x86_64:./tls/x86_64:./tls/x86_64:./tls:./x86_64/x86_64:./x86_64:./x86_64:. (RUNPATH from file ./test)
56327: trying file=./glibc-hwcaps/x86-64-v2/libc.so.6
56327: trying file=./tls/x86_64/x86_64/libc.so.6
56327: trying file=./tls/x86_64/libc.so.6
56327: trying file=./tls/x86_64/libc.so.6
56327: trying file=./tls/libc.so.6
56327: trying file=./x86_64/x86_64/libc.so.6
56327: trying file=./x86_64/libc.so.6
56327: trying file=./x86_64/libc.so.6
56327: trying file=./libc.so.6
56327: search cache=/etc/ld.so.cache
56327: trying file=/lib64/libc.so.6
56327:
56327:
56327: calling init: /lib64/ld-linux-x86-64.so.2
56327:
56327:
56327: calling init: /lib64/libc.so.6
56327:
56327:
56327: calling init: ./libt.so
56327:
56327:
56327: initialize program: ./test
56327:
56327:
56327: transferring control: ./test
56327:
i am in t.c
56327:
56327: calling fini: ./test [0]
56327:
56327:
56327: calling fini: ./libt.so [0]
56327:
可以明显的看到,rpath增加的不仅仅是一个路径,而是一组!.rpath指向的路径,是个basedir
!
前文有涉及Lazy Binding的内容,这是动态链接器的默认配置,当然也可以改变,即让LD_BIND_NOW
环境变量为1即可。
$ export LD_BIND_NOW=1
$ # or
$ LD_BIND_NOW=1 command...
此时,程序加载时所有外部接口的地址在进入入口前,都会全部确定。一般实时进程需要这样的设置!
本文链接:https://cs.pynote.net/sf/c/cdm/202307066/
-- EOF --
-- MORE --