Linux 动态库so的操作

Linux 动态库so的操作

Linux初学者并且对c语言不太熟悉的同学,在接触Linux的时候往往不知道用什么工具来操作linux的动态库.so文件,以及不知道.so文件应该放在哪里,遇到执行程序报错找不到xxx.so文件的时候应该怎么办。

于是乎,针对这些疑问,我写下这篇博文来总结一个动态库的一些操作,希望能帮助到大家。

🔍查看执行文件依赖哪些so文件

运行一个执行文件时,我们往往想要知道它依赖哪些so文件。我们可以使用下面几个方法来获取

1. ldd

这个命令应该是使用最广泛的了,只需要ldd命令后面跟上可执行文件,如:

$ ldd uselib                                            
linux-vdso.so.1 (0x00007ffc27709000)
libdistance.so => not found
libm.so.6 => /usr/lib/libm.so.6 (0x00007ff4bd2c0000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007ff4bd0de000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007ff4bd3ea000)

可以看到uselib命令依赖了linux-vdso.so.1、libdistance、libm.so.6、libc.so.6和/lib64/ld-linux-x86-64.so.2。其中ldd还列出了找到的so的实际位置(linux-vdso.so.1 是一种linux内核特殊的虚拟共享对象,没有实际物理位置)。

若一个执行文件的so有没找到的,则会向上文一样提示not found。

2. objdump

上文提到的ldd虽然方便快捷,但是在有些情况下其存在一些安全上的问题。

ldd的man手册中这么写到(已翻译):

请注意,在某些情况下(例如,程序指定了除了 ld-linux.so 之外的 ELF 解释器),一些版本的 ldd 可能会尝试通过直接执行程序来获取依赖信息,这可能导致执行程序中定义的任何代码,甚至执行程序本身。(在 glibc 2.27 之前,上游 ldd 实现就是这样做的,尽管大多数发行版提供了一个修改过的版本,不会这样做。)

因此,在处理不受信任的可执行文件时,您绝对不应该使用 ldd,因为这可能导致执行任意代码。

因此在处理不受信任的可执行文件时,更安全的替代方法是:

$ objdump -p /path/to/program | grep NEEDED

但是ldd还是有它的绝对优势的。ldd还可以搜索出动态库的依赖的动态库的情况,即间接引用,而objdump只能显示直接引用。

3. readelf

linux是如何知道一个文件需要哪些动态库的呢,其实一个执行程序的所有需要的库信息,都用字符串写进了这个执行程序的头部,并且在当代linux下,执行程序实际上也是有格式的,这个格式就是elf。

因此我们实际上可以从原理上出发,直接读取执行文件不就好了。readelf命令就是用来解析elf文件的好帮手。

可以用下面的命令来读取elf中动态链接的section,也就是依赖动态库标明的地方,字段是NEEDED。还是拿uselib命令举例🌰:

$ readelf -d uselib                                                                       
Dynamic section at offset 0x2dc0 contains 28 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libdistance.so]
0x0000000000000001 (NEEDED) Shared library: [libm.so.6]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x11a8
0x0000000000000019 (INIT_ARRAY) 0x3db0
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x3db8
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x3c0
0x0000000000000005 (STRTAB) 0x4a0
0x0000000000000006 (SYMTAB) 0x3e0
0x000000000000000a (STRSZ) 187 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x3fe8
0x0000000000000002 (PLTRELSZ) 48 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x660
0x0000000000000007 (RELA) 0x5a0
0x0000000000000008 (RELASZ) 192 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffb (FLAGS_1) Flags: PIE
0x000000006ffffffe (VERNEED) 0x570
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x55c
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0

前三行标注的就是我们想要的结果:

0x0000000000000001 (NEEDED)             Shared library: [libdistance.so]
0x0000000000000001 (NEEDED) Shared library: [libm.so.6]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

📢添加Linux动态库的搜索路径

在下文我们会讲述Linux默认会去哪些目录去搜索动态库。但是在次之前我更想讲讲如何添加搜索路径。

我所知道的有三种方式,一种编译的时候可动,另外两种运行的时候可动,分别为:

1. 编译的时候添加搜索路径

这个方法如标题所示,是在编译的时候就指定我这个执行程序运行的时候需要去哪些路径搜索动态库。

这个可以通过在链接的过程中,通过-rpath选项告诉ld链接器,ld链接器就会在最终生成的执行文件(我们说过了,这个是elf格式)的动态链接section的RUNPATH字段中著名,如下的中间部分:

$ readelf -d uselib                                                                                
Dynamic section at offset 0x2db0 contains 29 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libdistance.so]
...

0x000000000000001d (RUNPATH) Library runpath: [.]

....

0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0

注意-rpath选项并不会影响ld链接器在编译的时候搜索动态库的路径,如果要告诉ld链接器自己在编译的时候要去哪里搜索可以使用-L参数

由次可知,执行该uselib命令的时候不仅会搜索系统默认的动态库路径,还会搜索当前目录(即.)

2. 使用环境变量

我们可以通过LD_LIBRARY_PATH变量实现,就如PATH变量的使用方式一样,使用冒号分割多个路径。

$ LD_LIBRARY_PATH=/path/to/lib,./relative/path/lib uselib
or
$ export LD_LIBRARY_PATH=/path/to/lib,./relative/path/lib
$ uselib

3. 使用ldconfig相关命令

ldconfig命令会对

  • 在命令行中指定的pathl d c o n f ig
  • /etc/ld.so.conf文件中指定的path
  • 系统默认信任的文件夹/lib和/usr/lib

创建必要的链接和缓存,来加快ld搜索动态库。

因此我们可以在 /etc/ld.so.conf中永久添加我们想要添加的搜索path,一般config可以这么写:

# Dynamic linker/loader configuration.
# See ld.so(8) and ldconfig(8) for details.

# 可以使用include
include /etc/ld.so.conf.d/*.conf
# 或者直接指定
/usr/lib/libfakeroot

无论是config更新完还是指定的目录添加了新的so库之后,都需要使用ldconfig命令来重新构建缓存。否则会出现想要的lib not found的情况。

⚙Linux动态库默认的搜索顺序和搜索目录

上文我们说了如何自定义,接下来我们很有必要介绍默认系统会搜索哪些目录和搜索顺序,这样才能更好的自定义添加。

不过在此之前我们再插一嘴:

正如上文中提到的,Linux中,包括

  1. 读取即将运行的文件需要哪些动态库
  2. 查找动态库的位置
  3. 将动态库载入内存
  4. 真正的启动运行的文件

这些任务,实际上都是由ld.so来执行的。

因此要想知道这个问题,我们可以查阅ld.so的man手册,其中一个段落记录了ld.so的搜索顺序以及默认的文件夹

When resolving shared object dependencies, the dynamic linker first inspects each dependency string to see if it contains a slash (this can occur if a shared ob‐
ject pathname containing slashes was specified at link time). If a slash is found, then the dependency string is interpreted as a (relative or absolute) path‐
name, and the shared object is loaded using that pathname.

If a shared object dependency does not contain a slash, then it is searched for in the following order:

(1) Using the directories specified in the DT_RPATH dynamic section attribute of the binary if present and DT_RUNPATH attribute does not exist. Use of DT_RPATH
is deprecated.

(2) Using the environment variable LD_LIBRARY_PATH, unless the executable is being run in secure-execution mode (see below), in which case this variable is ig‐
nored.

(3) Using the directories specified in the DT_RUNPATH dynamic section attribute of the binary if present. Such directories are searched only to find those ob‐
jects required by DT_NEEDED (direct dependencies) entries and do not apply to those objects’ children, which must themselves have their own DT_RUNPATH en‐
tries. This is unlike DT_RPATH, which is applied to searches for all children in the dependcan shuency tree.

(4) From the cache file /etc/ld.so.cache, which contains a compiled list of candidate shared objects previously found in the augmented library path. If, how‐
ever, the binary was linked with the -z nodefaultlib linker option, shared objects in the default paths are skipped. Shared objects installed in hardware
capability directories (see below) are preferred to other shared objects.

(5) In the default path /lib, and then /usr/lib. (On some 64-bit architectures, the default paths for 64-bit shared objects are /lib64, and then /usr/lib64.)
If the binary was linked with the -z nodefaultlib linker option, this step is skipped.

翻译一下就是答案了。

其中有一些点我想要补充和提醒。

  • 我们可以发现/lib和/usr/lib目录作为兜底是无论如何都会去🔍搜索的,因此我们思考一个问题:

    刚刚我们说到如果 /etc/ld.so.conf配置文件有更改或/etc/ld.so.conf指定的文件夹中有新增动态库想生效的时候,我们需要ldconfig刷新缓存,我们同时又提到了ldconfig默认会去缓存/lib和/usr/lib。那么问题来了,如果我在/lib文件夹下面新增了一个动态库,不使用ldconfig命令刷新缓存,系统会提示找不到库吗?

答案是:不会。因为虽然第四步会去搜索ldconfig产生的缓存文件/etc/ld.so.cache,如果我新增了一个文件,ld只是在缓存中找不到。但是/usr/lib作为保底,ld还是会在第五步去搜索的。

  • DT_RPATH 已被废除,正如上文中我们展示的一样,默认现在ld -rpath,最终只会加DT_RUNPATH了。
  • 虽然ldconfig在开机的时候会自动启动(systemd的系统下是由ldconfig.service控制的),但是添加完新的so,记得指定ldconfig。