Linux动态链接装载的那些事儿

动态链接装载的步骤

在Linux上的可执行文件是一个ELF格式的文件。当我们遇到一个动态链接的可执行文件时,它是这样被执行的。

  1. 装载由LD_PRELOAD指定的动态链接库(通常为绝对路径)。
  2. 装载由/etc/ld.so.preload指定的动态链接库(通常为绝对路径)。
  3. 搜索满足该ELF文件中.dynamic节指定的NEEDED中指定的(一个或多个)动态链接库名或路径,之后装载。

对于1和2,装载动态链接库的操作是自然而然的。那么对于NEEDED,是如何找到对应的动态链接库的呢?首先如果这里的NEEDED字符串是一个相对或绝对路径,仍然不需要去搜索而只需要解析路径即可(注意,所有的相对路径均相对于当前的工作目录,而不是二进制文件相对的目录)。当动态链接库只包含了名称时,按照如下的搜索方式进行:

  1. 由ELF中的.dynamic指定的RPATH决定。注意两点:一是RPATH如今已经过时了,所以只有旧的ELF才会有这样的域;二是当.dynamic中有RUNPATH时这个域中指定的值会被忽略。
  2. 由环境变量LD_LIBRARY_PATH决定。这个环境变量生效的前提是该ELF处在非安全执行模式(non secure-execution mode)下,即ELF的AT_SECURE为0情况下------一般而言二进制可执行文件都是在这样的模式下执行的。
  3. 由ELF中的.dynamic指定的RUNPATH决定。需要注意的是,RUNPATH仅指定了直接依赖的搜索路径,搜索间接依赖的动态链接库时不会从RUNPATH中搜索(RPATH可用于直接或间接依赖的动态链接库搜索)。
  4. /etc/ld.so.cache指定。这个文件在我的系统上指定了一系列的动态链接库的绝对路径。需要注意的是,这个文件仅在ELF文件中NODEFLIB标志位不存在时才能生效------一般的ELF均不包含这个标志位。
  5. 默认路径/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
  • RUNPATHman设置了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.solibman-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.2ld.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/binfile -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文件不做任何修改,而在执行时按照用户指定的方式去执行对应的二进制文件。

其他参考链接

相关推荐
一只爱打拳的程序猿17 分钟前
【Spring】更加简单的将对象存入Spring中并使用
java·后端·spring
假装我不帅2 小时前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc
神仙别闹2 小时前
基于ASP.NET+SQL Server实现简单小说网站(包括PC版本和移动版本)
后端·asp.net
计算机-秋大田2 小时前
基于Spring Boot的船舶监造系统的设计与实现,LW+源码+讲解
java·论文阅读·spring boot·后端·vue
货拉拉技术3 小时前
货拉拉-实时对账系统(算盘平台)
后端
掘金酱3 小时前
✍【瓜分额外奖金】11月金石计划附加挑战赛-活动命题发布
人工智能·后端
代码之光_19803 小时前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi4 小时前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
颜淡慕潇4 小时前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决