前言:之前在503的时候经常因为动态库找不到而烦恼,今天这篇文章就对其查找顺序简单梳理下 ~~~

在 Linux 系统中,指定动态库搜索路径有多种方法,以下是五种常用的方法及它们的优先级顺序(从高到低)

针对一个程序想知道使用 ldd 命令来查看一个可执行文件或共享库的依赖库,并检查这些库是否能够正确加载。

动态库路径

示例

1
2
3
4
5
6
7
8
✘ ⚙ penge@penge-virtual-machine  ~/Desktop/test  ldd ./main
linux-vdso.so.1 (0x00007ffc160f8000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f4855d57000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f4855d32000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f4855d0f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4855b1d000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f48559ce000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4855fe6000)

这些动态库的信息在**.dynamic 段**。

在程序启动时指定(LD_PRELOAD 环境变量)

  • 优先级最高。使用 LD_PRELOAD 可以强制程序优先加载指定的动态库,而不是使用程序默认链接的库。这对于调试或替换特定库非常有用。
1
LD_PRELOAD=/path/to/custom/library.so ./your_program

在程序启动时指定(LD_LIBRARY_PATH 环境变量)

  • LD_LIBRARY_PATH 环境变量指定了动态链接器在运行时应该优先搜索的动态库路径。它覆盖默认路径,但低于 LD_PRELOAD 的优先级。
1
2
export LD_LIBRARY_PATH=/path/to/custom/libs:$LD_LIBRARY_PATH
./your_program

【特别注意】:

LD_LIBRARY_PATH 的设置只对当前 shell 会话以及从这个 shell 启动的子进程有效。如果你在一个新的终端或 shell 会话中,需要重新设置 LD_LIBRARY_PATH,或者将其设置到用户的环境配置文件中(如 ~/.bashrc~/.bash_profile)以使其永久生效。

.bashrc文件介绍

.bashrc,属于一种系统隐藏文件,.bashrc就是这个终端里面指令运行的配置脚本

当我们要配置.bashrc时,可以通过 nano 或者 vi / vim指令对.bashrc进行编辑,如下:

1
2
3
nano ~/.bashrc
vim ~/.bashrc
vi ~/.bashrc

对bashrc做出任何的修改,都将会在下一次启动终端时候生效。如果希望立即生效,可以执行source指令:

1
source ~/.bashrc

在链接时指定(-rpath 编译选项)

  • -rpath 是在编译时指定的选项,用于指定程序在运行时搜索动态库的路径。-rpath 的优先级高于默认路径和 /etc/ld.so.conf 配置的路径,但低于 LD_LIBRARY_PATH
1
gcc -o your_program your_program.c -Wl,-rpath,/path/to/custom/libs

在系统配置中指定(/etc/ld.so.conf/etc/ld.so.conf.d/

  • 通过修改 /etc/ld.so.conf 文件或在 /etc/ld.so.conf.d/ 目录中添加配置文件,可以指定系统全局的动态库搜索路径。需要在修改配置后运行 ldconfig 命令来更新缓存。
1
2
echo "/path/to/custom/libs" | sudo tee -a /etc/ld.so.conf.d/custom.conf
sudo ldconfig
  1. 默认搜索路径

    • 如果没有通过上述方法指定,系统会按照默认路径来搜索动态库,通常包括 /lib, /usr/lib, /lib64, /usr/lib64 等目录。

    • 直接运行程序即可,系统会根据默认路径加载动态库。

    1
    ./your_program

/etc/ld.so.conf

/etc/ld.so.conf 是系统全局配置文件,通常位于 /etc/ 目录下。它由系统管理员或安装包的脚本配置,用来为所有用户和进程设置额外的动态库搜索路径。

文件的内容是由每行一个路径组成的列表,每个路径代表一个要添加到动态链接器搜索路径中的目录。路径可以是绝对路径,也可以是包含变量的路径。

1
2
3
/usr/local/lib
/opt/custom/lib
/usr/lib/x86_64-linux-gnu

上面的示例将 /usr/local/lib/opt/custom/lib/usr/lib/x86_64-linux-gnu 添加到系统的动态库搜索路径中。

包含其他配置文件

/etc/ld.so.conf 文件还可以包含对其他配置文件的引用,这通常通过 include 关键字实现。这允许将不同的路径配置分离到多个文件中,便于管理。

示例

1
include /etc/ld.so.conf.d/*.conf

这行配置会将 /etc/ld.so.conf.d/ 目录下所有以 .conf 结尾的文件中的路径添加到动态链接器的搜索路径中。

补充:

动态链接库的大致过程介绍

1.ELF 文件与 ld.so 的启动

用户执行一个使用动态库的程序时,内核通过解析 ELF 文件的 Program Header 表来找到并启动 ld.so 动态链接器。

使用 readelf 工具查看 ELF 文件中的 Program Header

1
readelf -l /bin/ls

在输出中,可以看到包含动态段的入口,如 .interp 段,该段指定了动态链接器的位置(如 /lib64/ld-linux-x86-64.so.2)。

2.动态库的查找过程

去.dymaic查看所需的动态库!

ld.so 在加载可执行文件后,会根据多种方式查找所依赖的动态库,具体顺序如下:

  • LD_PRELOAD: 优先加载由 LD_PRELOAD 环境变量指定的库。
  • LD_LIBRARY_PATH: 然后查找由 LD_LIBRARY_PATH 环境变量指定的路径。
  • rpathrunpath: ELF 文件中指定的库搜索路径。
  • /etc/ld.so.confld.so.cache: 全局配置的库路径。
  • 默认路径: 系统默认的 /lib/usr/lib 等路径。

3. 符号解析与重定位

ld.so 在加载库后,负责解析这些库中的符号(如函数和全局变量),并进行重定位,使得程序可以正确调用库中的函数或访问库中的数据。

  • 符号表ld.so 从 ELF 文件中的 .dynsym 段加载符号表,并根据符号名在所有加载的库中查找对应的符号地址。
  • 重定位:对于未确定的地址(符号位置),ld.so 根据实际加载库的内存地址对其进行重定位。

例子:使用 GDB 查看符号表和重定位条目

1
2
3
gdb /usr/bin/curl
(gdb) info functions # 查看所有函数符号
(gdb) info variables # 查看所有变量符号

4. 延迟绑定(Lazy Binding)

为了提升性能,ld.so 采用延迟绑定机制,只有在函数首次调用时才进行符号解析。这种方式减少了程序启动时的开销。

  • PLT(Procedure Linkage Table):用于实现延迟绑定的表,每次函数调用时,先跳转到 PLT 表中的入口,该入口再调用动态链接器解析函数的实际地址。
  • GOT(Global Offset Table):保存已解析的函数地址,当再次调用时,直接从 GOT 中获取函数地址,避免重复解析。
1
objdump -d -j .plt /usr/bin/curl

objdump命令是用查看目标文件或者可执行的目标文件的构成的gcc工具。

补充:linux程序启动

Linux操作系统下应用程序的启动过程和Win32有类似之处,在 Linux 下应用程序加载和执行的具体步骤如下。

第1步:用户在 linux命令终端中输入运行程序的命令,然后按回车键。

第2步:首先接管的是 exec系统调用,它会为应用程序的运行准备一些环境变量等,并且为运行的命令找到相应的解释器。

第3步:通常应用程序的解释器就是ld (loader/加载器),ld接管控制权后首先需要读入这个可执行程序的文件的一部分,包括文件头及共享对象(so,对应于Windows下的动态链接库)区等。然后检查这个可执行文件所依赖的共享对象so(这些信息都在可执行文件中),并且在 LD_LIBRARY_PATH和系统默认库文件夹的位置查找这些库是否存在。如果不存在,则报告错误并退出执行。

第4步:针对每一个依赖的库,ld需要首先读入这个so的一部分文件头和相关信息。然后递归查找该共享对象所依赖的其他共享对象,直到最底层。在这个过程中,ld 会在内部维护一个数据结构用来记录所有这些共享对象之间的相互依赖关系。最终,ld会确认所有的该可执行程序直接或间接依赖的so都存在。

第5步:ld会把所有依赖的so映射到该程序的进程空间的虚拟内存中(只是映射,并不是把全部 so文件的内容读入内存)。显然,每一个共享对象在该进程的虚拟内存空间中占据不同的连续区域。它们的“基地址”各不相同,从而其内部的一些用绝对地址表示的符号需要做出相应的修改。这个过程称为“relocation过程”。

第6步:初始化应用程序的全局变量,对于全局对象自动调用构造函数。第7步:进入main函数开始执行。

优化动态库

减少动态链接库的数量

减少动态链接库的数量是提高程序启动性能的一个重要原则,是构造大型应用程序时需要考虑的首要问题。例如微软的Office 套件程序启动时只需要两个动态链接库。根据实践经验,如果一个应用程序启动时需要加载的动态链接库达到数十个,则把这些动态库的数量减少到10个以内,至少可以提高启动性能达15%。在性能优化工程实践中,减少应用程序启动时需要加载的动态链接库数量可以通过如下两种方法来实现。

1.修改代码

2.合并动态库

把功能相近或者依赖关系密切的动态库合并成一个库。我认为是减少读盘次数

减小动态链接库尺寸

使用 strip 命令:

  • strip 命令可以从目标文件或者可执行文件中删除符号表信息,从而减小文件大小。

  • 示例命令:

    1
    strip -s libfoo.so
  • 这将从 libfoo.so 动态库中删除符号表信息,从而缩小文件大小。

原理

  1. 可执行程序在链接时,只需要使用动态库中的符号信息,而不需要完整的符号表信息。
  2. 当可执行程序在运行时,需要动态加载动态库并解析其中的符号信息。即使动态库中没有完整的符号表信息,但仍然可以正确解析所需的符号。

所以通常情况下,使用 strip -s 命令删除动态库中的符号表信息,不会影响该动态库的正常使用。这种方式可以有效地减小动态库的文件大小,而不会影响应用程序的正常运行。