动态链接库的基本介绍

ld.so

ld.so的路径存在于任意一个存在动态链接库的elf文件上的.interp setion中

# objdump -s -j .interp /bin/ls
/bin/ls: file format elf64-x86-64

Contents of section .interp:
02a8 2f6c6962 36342f6c 642d6c69 6e75782d /lib64/ld-linux-
02b8 7838362d 36342e73 6f2e3200 x86-64.so.2.

#readelf -a /bin/ls | grep 'interpreter'
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

任何动态链接的程序都需要ld.so去启动,即直接执行一个动态链接库程序等价于执行 ld.so <program>

调试

查看动态库的so-name和依赖的动态库

使用readelf命令查看.dynamic

readelf -d thirdparty/lib/libluajit-5.1.so.2

Dynamic section at offset 0x80d90 contains 28 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libm.so.6]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x0000000000000001 (NEEDED) Shared library: [ld-linux-x86-64.so.2]
0x000000000000000e (SONAME) Library soname: [libluajit-5.1.so.2]

使用ldd命令可以查看依赖动态库的查找情况

ldd thirdparty/lib/libluajit-5.1.so.2
linux-vdso.so.1 (0x00007ffc87f1d000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x0000748ad81c6000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x0000748ad81a6000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000748ad7e00000)
/lib64/ld-linux-x86-64.so.2 (0x0000748ad833d000)

从上边的输出,可以看到每个依赖的动态库找到的位置

动态库查找过程

ldconfig

如果动态链接器在每次查找共享库时都去遍历这些目录,那将会非常耗费时间。

所以Linux系统中都有一个叫做ldconfig的程序,这个程序的作用是为共享库目录下的各个共享库创建、删除或更新相应的SO-NAME(即相应的符号链接),这样每个共享库的SO-NAME就能够指向正确的共享库文件;

并且这个程序还会将这些SO-NAME收集起来,集中存放到/etc/ld.so.cache文件里面,并建立一个SO-NAME的缓存。当动态链接器要查找共享库时,它可以直接从/etc/ld.so.cache里面查找。

而/etc/ld.so.cache的结构是经过特殊设计的,非常适合查找,所以这个设计大大加快了共享库的查找过程。

详见man ldconfig

/etc/ld.so.conf

系统的默认查找顺序由/etc/ld.so.conf指定

我这里的配置文件内容是

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

/etc/ld.so.conf.d/ 目录中的文件顺序通常由文件系统的实现和具体的 ldconfig 实现决定。一般来说,文件系统会按照文件名的字典顺序(lexicographical order)来处理文件。ldconfig 会读取 /etc/ld.so.conf 文件中的内容,并递归地读取 /etc/ld.so.conf.d/ 目录中的所有文件。

查看当前系统默认动态库的查找顺序

sudo ldconfig -v 2>/dev/null | grep -v '^$' | grep -E ':$'
/usr/lib/x86_64-linux-gnu/libfakeroot:
/usr/local/lib:
/lib/x86_64-linux-gnu:
/lib:

查找顺序

如果一个共享对象依赖项不包含斜杠,则按照以下顺序进行搜索:

  1. 使用ELF文件的 DT_RPATH 动态节属性中指定的目录(如果存在,并且 DT_RUNPATH 属性不存在)注: DT_RPATH的使用已被弃用
  2. 使用环境变量 LD_LIBRARY_PATH,除非可执行文件在安全执行模式下运行,在这种情况下该变量将被忽略。
  3. 使用elf文件的 DT_RUNPATH 动态节属性中指定的目录。这些目录仅用于查找 DT_NEEDED(直接依赖项)条目所需的对象,而不适用于这些对象的子对象,这些子对象必须自己有DT_RUNPATH条目。这与 DT_RPATH 不同,后者适用于依赖树中所有子对象的搜索
  4. 从缓存文件 /etc/ld.so.cache 中搜索,该文件包含先前在扩展库路径中找到的候选共享对象的编译列表。
  5. 默认路径 /lib 中搜索,然后在 /usr/lib 中搜索。(在某些64位架构上,64位共享对象的默认路径是/lib64,然后是/usr/lib64。)如果二进制文件是使用-z nodeflib链接器选项链接的,则此步骤将被跳过。

详细可以见man ld.so

环境变量

LD_LIBRARY_PATH

Linux系统提供了很多方法来改变动态链接器装载共享库路径的方法,通过使用这些方法,我们可以满足一些特殊的需求,比如共享库的调试和测试、应用程序级别的虚拟等。

改变共享库查找路径最简单的方法是使用LD_LIBRARY_PATH环境变量,这个方法可以临时改变某个应用程序的共享库查找路径,而不会影响系统中的其他程序。

LD_LIBRARY_PATH=/home/user /bin/ls

LD_LIBRARY_PATH对于共享库的开发和测试来说十分方便,但是它不应该被滥用。也就是说,普通用户在正常情况下不应该随意设置LD_LIBRARY_PATH来调整共享库搜索目录。

Linux中还有一种方法可以实现与LD_LIBRARY_PATH类似的功能,那就是直接运行动态链接器来启动程序,比如:

/lib/ld-linux.so.2 –library-path /home/user /bin/ls

LD_PRELAOD

可以指定预先装载的一些共享库甚或是目标文件

由于全局符号介入这个机制的存在,LD_PRELOAD里面指定的共享库或目标文件中的全局符号就会覆盖后面加载的同名全局符号,这使得我们可以很方便地做到改写标准C库中的某个或某几个函数而不影响其他函数,对于程序的调试或测试非常有用。

与LD_LIBRARY_ PATH一样,正常情况下应该尽量避免使用LD_PRELOAD,比如一个发布版本的程序运行不应该依赖于LD_PRELOAD。

LD_PRELOAD=/path/to/libfoo.so ./my_program

系统配置文件中有一个文件是/etc/ld.so.preload,它的作用与LD_PRELOAD一样。这个文件里面记录的共享库或目标文件的效果跟LD_PRELOAD里面指定的一样,也会被提前装载。

LD_DEBUG

这个变量可以打开动态链接器的调试功能,当我们设置这个变量时,动态链接器会在运行时打印出各种有用的信息,对于我们开发和调试共享库有很大的帮助。

比如我们可以将LD_DEBUG设置成“files”,并且运行一个简单动态链接的HelloWorld:

$LD_DEBUG=files ./HelloWorld.out
12118:
12118: file=libc.so.6 [0]; needed by ./HelloWorld.out [0]
12118: file=libc.so.6 [0]; generating link map
12118: dynamic: 0xb7f16d9c base: 0xb7dd1000 size: 0x00149610
12118: entry: 0xb7de71b0 phdr: 0xb7dd1034 phnum: 10
12118:
12118:
12118: calling init: /lib/tls/i686/cmov/libc.so.6
12118:
12118:
12118: initialize program: ./HelloWorld.out
12118:
12118:
12118: transferring control: ./HelloWorld.out
12118:
Hello world
12118:
12118: calling fini: ./HelloWorld.out [0]
12118:
12118:
12118: calling fini: /lib/tls/i686/cmov/libc.so.6 [0]
12118:

动态链接器打印出了整个装载过程,显示程序依赖于哪个共享库并且按照什么步骤装载和初始化,共享库装载时的地址等。

LD_DEBUG还可以设置成其他值,比如:

  • “bindings”显示动态链接的符号绑定过程。
  • “files” 显示程序依赖于哪个共享库并且按照什么步骤装载和初始化,共享库装载时的地址
  • “libs” 显示共享库的查找过程。
  • “versions” 显示符号的版本依赖关系。
  • “reloc” 显示重定位过程。
  • “symbols” 显示符号表查找过程。
  • statistics” 显示动态链接过程中的各种统计信息。

动态库创建

编译动态库

最关键的是使用GCC的两个参数,即“-shared”和“-fPIC”。

“-shared”表示输出结果是共享库类型的;“-fPIC”表示使用地址无关代码(Position Independent Code)技术来生产输出文件。

如果需要指定so-name

$gcc –shared –Wl,-soname,my_soname –o library_name source_files library_files

注意,只有指定so-name,ldconfig才会有效果

可以使用链接器的rpath选项指定链接产生的目标程序的共享库查找路径

$ld –rpath /home/mylib –o program.out program.o –lsomelib

链接是保留全部符号

默认情况下,链接器在产生可执行文件时,只会将那些链接时被其他共享模块引用到的符号放到动态符号表,这样可以减少动态符号表的大小。

也就是说,在共享模块中反向引用主模块中的符号时,只有那些在链接时被共享模块引用到的符号才会被导出。

有一种情况是,当程序使用dlopen()动态加载某个共享模块,而该共享模块须反向引用主模块的符号时,有可能主模块的某些符号因为在链接时没有被其他共享模块引用而没有被放到动态符号表里面,导致了反向引用失败。

ld链接器提供了一个“--export-dynamic”的参数(或者直接使用-E),这个参数表示链接器在生产可执行文件时,将所有全局符号导出到动态符号表,以防止出现上述问题。我们也可以在GCC中使用“**-Wl,-export-dynamic**”将该参数传递给链接器。

—export-dynamic-symbol, —export-dynamic-symbol-list 两个选项提供了更灵活的导出符号功能,可以指定符号的模式或者列表。

清除符号信息

$strip libfoo.so

去除符号和调试信息以后的文件往往比之前要小很多,一般只有原来的一半大小,甚至不到一半。

除了使用“strip”工具,我们还可以使用ld的“-s”和“-S”参数,使得链接器生成输出文件时就不产生符号信息。

“-s”和“-S”的区别是:“-S”消除调试符号信息,而“-s”消除所有符号信息。

我们也可以在gcc中通过“-Wl,-s”和“-Wl,-S”给ld传递这两个参数。

安装动态库

安装动态库只需要把他们复制到系统目录(/etc/ld.so.conf 中),可选执行ldconfig刷新缓存加快访问

除此之外,还可以直接利用ldconfig来指定一个外部的目录

$ldconfig –n /xxxx/shared_library_directory

不建议用环境变量的方式去指定,这种方式应该只用于调试目的

一些问题

  1. 问:是否可以修改编译后的动态链接库文件,比如修改so-name, r-path之类的?

    答:可以使用下边的patchelf项目

https://github.com/NixOS/patchelf

当只有第三方的动态库二进制文件而没有源码,需要修改特定的配置的时候可以使用这个方式(常修改的有RPATH, 动态库位置的查询目录)

作者

deepwzh

发布于

2024-12-19

更新于

2025-01-16

许可协议

评论