动态链接装载的步骤
在Linux上的可执行文件是一个ELF格式的文件。当我们遇到一个动态链接的可执行文件时,它是这样被执行的。
- 装载由
LD_PRELOAD
指定的动态链接库(通常为绝对路径)。 - 装载由
/etc/ld.so.preload
指定的动态链接库(通常为绝对路径)。 - 搜索满足该ELF文件中
.dynamic
节指定的NEEDED
中指定的(一个或多个)动态链接库名或路径,之后装载。
对于1和2,装载动态链接库的操作是自然而然的。那么对于NEEDED
,是如何找到对应的动态链接库的呢?首先如果这里的NEEDED
字符串是一个相对或绝对路径,仍然不需要去搜索而只需要解析路径即可(注意,所有的相对路径均相对于当前的工作目录,而不是二进制文件相对的目录)。当动态链接库只包含了名称时,按照如下的搜索方式进行:
- 由ELF中的
.dynamic
指定的RPATH
决定。注意两点:一是RPATH如今已经过时了,所以只有旧的ELF才会有这样的域;二是当.dynamic
中有RUNPATH
时这个域中指定的值会被忽略。 - 由环境变量
LD_LIBRARY_PATH
决定。这个环境变量生效的前提是该ELF处在非安全执行模式
(non secure-execution mode)下,即ELF的AT_SECURE
为0情况下------一般而言二进制可执行文件都是在这样的模式下执行的。 - 由ELF中的
.dynamic
指定的RUNPATH
决定。需要注意的是,RUNPATH
仅指定了直接依赖
的搜索路径,搜索间接依赖的动态链接库时不会从RUNPATH
中搜索(RPATH
可用于直接或间接依赖的动态链接库搜索)。 - 由
/etc/ld.so.cache
指定。这个文件在我的系统上指定了一系列的动态链接库的绝对路径。需要注意的是,这个文件仅在ELF文件中NODEFLIB
标志位不存在时才能生效------一般的ELF均不包含这个标志位。 - 默认路径
/lib
,/lib64
,/usr/lib
,/usr/lib64
等等。这也要求在ELF文件中NODEFLIB
标志位不存在时才能生效。
使用readelf
我们可以查看一些ELF文件中的信息。比如执行readelf -d /usr/bin/man
会得到如下输出:
bash
Dynamic section at offset 0x1b6b0 contains 32 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libmandb-2.11.2.so]
0x0000000000000001 (NEEDED) Shared library: [libman-2.11.2.so]
0x0000000000000001 (NEEDED) Shared library: [libz.so.1]
0x0000000000000001 (NEEDED) Shared library: [libpipeline.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000001d (RUNPATH) Library runpath: [/usr/lib/man-db]
0x000000000000000c (INIT) 0x5000
0x000000000000000d (FINI) 0x15664
0x0000000000000019 (INIT_ARRAY) 0x1c630
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x1c638
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x3b0
0x0000000000000005 (STRTAB) 0x1928
0x0000000000000006 (SYMTAB) 0x3f8
0x000000000000000a (STRSZ) 2990 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x1c8f0
0x0000000000000002 (PLTRELSZ) 4776 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x3c40
0x0000000000000007 (RELA) 0x2740
0x0000000000000008 (RELASZ) 5376 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000000000001e (FLAGS) BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW PIE
0x000000006ffffffe (VERNEED) 0x26a0
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x24d6
0x000000006ffffff9 (RELACOUNT) 201
0x0000000000000000 (NULL) 0x0
-
RUNPATH
:man
设置了RUNPATH
这个变量:0x000000000000001d (RUNPATH) Library runpath: [/usr/lib/man-db]
。 -
NEEDED
:使用ls -l /usr/lib/man-db
得到
bash
total 232K
-rw-r--r-- 1 root root 197K Jul 24 2023 libman-2.11.2.so
-rw-r--r-- 1 root root 31K Jul 24 2023 libmandb-2.11.2.so
lrwxrwxrwx 1 root root 18 Jul 24 2023 libmandb.so -> libmandb-2.11.2.so
lrwxrwxrwx 1 root root 16 Jul 24 2023 libman.so -> libman-2.11.2.so
lrwxrwxrwx 1 root root 13 Jul 24 2023 man -> ../../bin/man*
lrwxrwxrwx 1 root root 15 Jul 24 2023 mandb -> ../../bin/mandb*
在这个目录下我们发现了readelf -d /usr/bin/man
结果中NEEDED
项的libman-2.11.2.so
和libman-2.11.2.so
(这里的NEEDED
像是指直接 依赖的动态链接库不带路径的库名。)。事实也是如此,在man执行时就是根据RUNPATH和NEEDED去找这两个动态链接库的。ldd /usr/bin/man | grep /usr/lib/man-db
的输出为:
ini
libmandb-2.11.2.so => /usr/lib/man-db/libmandb-2.11.2.so (0x00007f3512c0e000)
libman-2.11.2.so => /usr/lib/man-db/libman-2.11.2.so (0x00007f3512acf000)
这实际上是解析了软链接后得到的真实.so文件的路径。
ldd
的输出
使用ldd
可以查看某个具体的二进制可执行文件或动态链接库(.so
)直接或间接依赖的别的动态链接库。比如对于/usr/bin/man
,使用ldd
(它是一个shell脚本)可以识别出所有的依赖的动态链接库:
bash
$ ldd /usr/bin/man
linux-vdso.so.1 (0x00007ffd6a9f1000)
libachk.so => /lib/libachk.so (0x00007fc00f000000)
libmandb-2.11.2.so => /usr/lib/man-db/libmandb-2.11.2.so (0x00007fc00f175000)
libman-2.11.2.so => /usr/lib/man-db/libman-2.11.2.so (0x00007fc00f144000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007fc00f125000)
libpipeline.so.1 => /lib/x86_64-linux-gnu/libpipeline.so.1 (0x00007fc00f118000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc00ec00000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fc00f111000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fc00f10c000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007fc00f107000)
libgdbm.so.6 => /lib/x86_64-linux-gnu/libgdbm.so.6 (0x00007fc00efee000)
libseccomp.so.2 => /lib/x86_64-linux-gnu/libseccomp.so.2 (0x00007fc00efce000)
/lib64/ld-linux-x86-64.so.2 (0x00007fc00f2bd000)
- 这里
=>
的项指的是所依赖的库。如libmandb-2.11.2.so => /usr/lib/man-db/libmandb-2.11.2.so (0x00007fc00f175000)
和libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc00ec00000)
都是这类形式。 - 没有
=>
的项有linux-vdso.so.1
和/lib64/ld-linux-x86-64.so.2
。其中file -b /lib64/ld-linux-x86-64.so.2
的结果为/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
,但linux-vdso.so.1
并不是一个绝对路径。
/lib64/ld-linux-x86-64.so.2
(ld.so)
这对应着ld.so
(一般位置为/usr/bin/ld.so
),它是包含动态链接库的ELF格式的二进制可执行文件的解释器 /链接库装载器 ,在ELF述语里它被称为interpreter(解释器)。在我的机器上ld.so
是一个软链接,其指向了/lib64/ld-linux-x86-64.so.2
;它最终指向了/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
这样一个ELF文件。
bash
$ which ld.so
/usr/bin/ld.so
$ file -b /usr/bin/ld.so
symbolic link to /lib64/ld-linux-x86-64.so.2
$ file -b /lib64/ld-linux-x86-64.so.2
symbolic link to /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
# 后文我们会了解到,这个文件事实上不是一个动态链接库,而是一个二进制可执行文件!
$ file -b /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=2a8cae7d0d0330d889df3265f8908abb8f19255f, stripped
一般而言,ELF可执行文件中设置了interpreter。这可以使用readelf -l /usr/bin/man
来验证。
bash
...
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
...
当我们执行一个依赖动态链接库的二进制可执行文件时,事实上是使用了该可执行文件指定的interpreter去调用 /解释 了该可执行文件,先解决动态链接库的问题,然后执行可执行文件的逻辑。比如在执行/usr/bin/man man
时,等价于执行了/usr/bin/ld.so /usr/bin/man man
(或者说是/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /usr/bin/man man
)。当然我们可以强制指定ld.so
。比如下面的命令同样可以得到类似于man man
的结果。
bash
$ cp /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /tmp/ld.so
$ /tmp/ld.so /usr/bin/man man
此外注意几点:
- 这里的
ld.so
和我们平时所说的ld
命令不是一个命令 。ld
是链接器 ,在构建二进制 时用到;一般默认为ld.bfd
(man ld.bfd
)。ld.so
在运行时用到。
bash
$ echo 'int main() { return 0; }' > /tmp/main.c
$ clang /tmp/main.c -v -o /tmp/main.out
...
"/usr/bin/ld" -z relro --hash-style=gnu --eh-frame-hdr -m elf_x86_64 -pie -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o /tmp/main.out /lib/x86_64-linux-gnu/Scrt1.o /lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_6
4-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib64 -L/lib/x86_64-linux-gnu -L/lib/../lib64 -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib64 -L/lib -L/
usr/lib /tmp/main-7ac502.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /lib/x86_64-linux-gnu/crtn.o
/usr/bin/ld
就是在链接时 用到的;同时我们也能看到-dynamic-linker /lib64/ld-linux-x86-64.so.2
在这里指定了运行时 的解释器。对于gcc,使用gcc /tmp/main.c -v -o /tmp/main.out
最后会调用/usr/libexec/gcc/x86_64-linux-gnu/13/collect2
,可认为它自身也使用了ld。
- interpreter仅针对二进制可执行文件有意义,对于动态链接库没有意义。
readelf -l /path/to/bin
或file -b /path/to/bin
都可以查看可执行二进制的interpreter,但对于动态链接库是没有这个信息的。
bash
$ file -b /usr/bin/man
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=93e22c77b20c329ec51dad530c40fd0d64fc696f, for GNU/Linux 3.2.0, stripped
- interpreter一般设置为绝对路径(
$ORIGIN
只能被interpreter本身所解析,所以不能用于设置interpreter相对二进制文件的位置);如果设置为相对位置,则相对的是当前工作目录
的位置而不是二进制文件的位置。
bash
echo 'int main() { return 0; }' > main.c
gcc -Wl,--dynamic-linker=./lib/ld-linux-x86-64.so.2 -o main main.c
mkdir bin
mkdir lib
cp /lib64/ld-linux-x86-64.so.2 lib/
cp main bin/
bin/main # 正常运行
cd bin; ./main # ENOENT错误("No such file or directory")
-
ldd
事实上是ld.so
的一个wrapper,其核心调用方式为LD_TRACE_LOADED_OBJECTS=1 ld.so /usr/bin/man
(使用ld.so --list /usr/bin/man
也可达到类似效果)。ldd给出的信息是所有依赖的动态链接库 ,包括直接 依赖的动态链接库和间接依赖的动态链接库(如a.so依赖b.so,则b.so也会被列出来)。 -
ld.so
自身必然是一个静态链接的文件:毕竟它来解决所有的动态链接的任务嘛。
bash
$ ldd $(which ld.so)
statically linked
# file的结果显示dynamically linked,事实上是错误的
$ file -bL $(which ld.so)
ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=2a8cae7d0d0330d889df3265f8908abb8f19255f, stripped
linux-vdso.so.1
通过find /lib -name '*vdso*.so*'
命令,我们得到:
bash
/usr/lib/modules/6.2.0-32-generic/vdso/vdso32.so
/usr/lib/modules/6.2.0-32-generic/vdso/vdso64.so
/usr/lib/modules/6.5.0-28-generic/vdso/vdso32.so
/usr/lib/modules/6.5.0-28-generic/vdso/vdso64.so
/usr/lib/modules/5.4.0-94-generic/vdso/vdso32.so
/usr/lib/modules/5.4.0-94-generic/vdso/vdsox32.so
/usr/lib/modules/5.4.0-94-generic/vdso/vdso64.so
...
结合uname -r
结果6.2.0-32-generic
,可以猜测对应的.so
为/usr/lib/modules/6.2.0-32-generic/vdso/vdso64.so
。这里vDSO是virtual dynamic shared object的缩写,它是一种Linux内核自动映射到用户态应用地址空间的动态链接库;常Linux下的应用都会用它,一般不需要关心。具体详见man 7 vdso
。
指定/修改动态链接信息
我们可以通过在链接时 指定动态链接的信息,也可以在运行前 对ELF文件进行修改,同时还可以在运行时进行指定。
链接时指定:ld
在链接时,我们可以指定链接信息,这样使得在生成 二进制ELF文件时的链接信息就符合用户要求。比如Linux下默认的ld.bfd
或gold linker ld.gold
提供了设置RUNPATH、interpreter等的选项。
-rpath DIR
:设置DIR为RUNPATH;--dynamic-linker=FILE
设置FILE为interpreter。
当然一般我们不会直接使用链接器,而是通过编译器驱动程序gcc或clang时,通过选项来调用链接器。在下面的三种使用方式中,前两个gcc的调用方式(clang同理)和第三个用ld的方式效果是等价的:
gcc -Wl,aaa,bbb,ccc
gcc -Wl,aaa -Wl,bbb -Wl,ccc
ld aaa bbb ccc
当然,当我们使用make/cmake时,我们配置LDFLAGS
就可以了,而不需要直接指定gcc/clang的选项。如LDFLAGS="-Wl,-rpath,/usr/lib/man-db"就是在链接时指定了RUNPATH
为/usr/lib/man-db
。
运行前修改:patchelf
patchelf是一个可以打印ELF文件信息并可以直接对ELF文件进行修改的工具,相比chrpath更为强大。
它可以打印一些内容而不做任何修改。
bash
$ patchelf --print-interpreter /usr/bin/man
/lib64/ld-linux-x86-64.so.2
$ patchelf --print-rpath /usr/bin/man
/usr/lib/man-db
$ patchelf --print-soname /usr/lib/x86_64-linux-gnu/libz3.so
libz3.so.4
当然,它最强大的功能是对ELF文件做修改。这在我们遇到一台机器上构建好的二进制可执行文件移植到另一台机器上执行时,尤为有效! 事实上,patchelf是NixOS下的一个重要开源项目。
bash
$ 在/tmp/elf-test目录下实验
$ mkdir /tmp/elf-test
$ cp /usr/bin/man /tmp/elf-test
$ cd /tmp/elf-test && mkdir man-db
$ ldd ./man
linux-vdso.so.1 (0x00007ffc1266e000)
libachk.so => /lib/libachk.so (0x00007f5803700000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f58038df000)
libmandb-2.11.2.so => /usr/lib/man-db/libmandb-2.11.2.so (0x00007f58038d7000)
libman-2.11.2.so => /usr/lib/man-db/libman-2.11.2.so (0x00007f58038a6000)
libpipeline.so.1 => /lib/x86_64-linux-gnu/libpipeline.so.1 (0x00007f5803899000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5803400000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f5803892000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f580388d000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f5803888000)
libgdbm.so.6 => /lib/x86_64-linux-gnu/libgdbm.so.6 (0x00007f5803876000)
libseccomp.so.2 => /lib/x86_64-linux-gnu/libseccomp.so.2 (0x00007f5803856000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5803a45000)
# 把interpreter拷贝过来
$ cp /lib64/ld-linux-x86-64.so.2 /tmp/elf-test/ld.so
# 把其他非vdso的库拷贝过来
$ 把/usr/lib/man-db目录下的文件拷贝到 /tmp/elf-test/man-db下
$ cp /usr/lib/man-db/libmandb-2.11.2.so /usr/lib/man-db/libman-2.11.2.so /tmp/elf-test/man-db
$ 把其他库拷贝到 /tmp/elf-test下
$ cp /lib/libachk.so /lib/x86_64-linux-gnu/libz.so.1 /lib/x86_64-linux-gnu/libpipeline.so.1 /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libdl.so.2 /lib/x86_64-linux-gnu/libpthread.so.0 /lib/x86_64-linux-gnu/librt.so.1 /lib/x86_64-linux-gnu/libgdbm.so.6 /lib/x86_64-linux-gnu/libseccomp.so.2 .
# 设置interpter为/tmp/elf-test/ld.so
$ patchelf --set-interpreter $PWD/ld.so man
# 设置 RUNPATH
$ patchelf --set-rpath '$ORIGIN'/.:'$ORIGIN'/man-db
$ readelf -d ./man | grep RUNPATH
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/.:$ORIGIN/man-db]
$ ldd ./man
linux-vdso.so.1 (0x00007ffe351fc000)
libachk.so => /tmp/elf-test/././libachk.so (0x00007f0585d00000)
libz.so.1 => /tmp/elf-test/././libz.so.1 (0x00007f0585e55000)
libmandb-2.11.2.so => /tmp/elf-test/./man-db/libmandb-2.11.2.so (0x00007f0585e4d000)
libman-2.11.2.so => /tmp/elf-test/./man-db/libman-2.11.2.so (0x00007f0585e1c000)
libpipeline.so.1 => /tmp/elf-test/././libpipeline.so.1 (0x00007f0585e0f000)
libc.so.6 => /tmp/elf-test/././libc.so.6 (0x00007f0585a00000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0585e08000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f0585cdc000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f0585cd7000)
libgdbm.so.6 => /lib/x86_64-linux-gnu/libgdbm.so.6 (0x00007f0585cc5000)
libseccomp.so.2 => /lib/x86_64-linux-gnu/libseccomp.so.2 (0x00007f0585ca5000)
/tmp/elf-test/ld.so => /lib64/ld-linux-x86-64.so.2 (0x00007f0585f9e000)
$ man man # 可以正常运行
运行时指定:ld.so
如前文所述,对于一般的二进制文件,执行/path/to/bin $@
等价于于该二进制文件中指定的interpreter来显式执行/path/to/interpreter /path/to/bin $@
,而这里的/path/to/interpreter即是ld.so
;显式执行时,不需要再去看二进制文件中的interpreter。这样,我们可以手动指定特定的interpreter去执行对应的二进制文件。
此外,ld.so还可以指定一些选项,来约束运行时的参数。
- 使用
ld.so --library-path /dir/to/so /path/to/bin
中的--library-path
可以覆写LD_LIBRARY_PATH
。 - 使用
ld.so --inhibit-cache /path/to/bin
可以禁用/etc/ld.so.cache
。 - 使用
ld.so --preload LIST /path/to/bin
相当于覆写/etc/ld.so.preload
。 - 使用
ld.so --inhibit-rpath LIST /path/to/bin
可以禁用/path/to/bin
中RPATH或RUNPATH指定的动态链接库。
这事实上可以在链接时或链接后对二进制ELF文件不做任何修改,而在执行时按照用户指定的方式去执行对应的二进制文件。