详解共享库的动态加载

在本文中,我将尝试解释在Linux系统中动态加载共享库的内部工作原理。

这边文章不是一个如何引导,尽管它确实展示了如何编译和调试共享库和可执行文件。为了解动态加载的内部工作方式进行了优化。写这篇文章是为了消除我在该主题上的知识欠缺,以便成为一名更好的程序员。我希望它也能帮助您变得更好。

什么是共享库

库是一个包含编译后的代码和数据的文件。一般来说,库非常有用,因为它们可以缩短编译时间(在编译应用程序时不必编译依赖关系的所有源代码)和模块化开发过程。

静态库链接到已编译的可执行文件(或另一个库)中。编译后,新组件将包含静态库的内容。

共享库在运行时由可执行文件(或其他共享库)加载。这让它们变得更加复杂,通常大家对这个领域可能存在认知障碍,我们将在这篇文章中讨论。

示例设置

为了探索共享库的世界,我们将在本文中使用一个示例。我们将从三个源文件开始:

main.cpp是我们定义的可执行文件的主文件, 它不会做太多, 只是从我们将要编译的随机库random调用一个函数:

$ vi main.cpp

#include "random.h"

int main() {
    return get_random_number();
}

头文件random.h将定义一个简单的函数:

$ vi random.h

int get_random_number();

它将在其源文件中提供一个简单的实现, random.cpp

$ vi random.cpp

#include "random.h"

int get_random_number(void) {
    return 4;
}

Note: 所有示例均在Ubuntu 14.04系统上运行

编译共享库

在编译实际库之前,我们将从random.cpp创建一个目标文件:

$ clang++ -o random.o -c random.cpp

通常,一切正常后,构建工具不会打印到标准输出。以下是所有解释的参数:

  • -o random.o: 将输出文件名定义为random.

  • -c: 不尝试任何链接(只编译)

  • random.cpp: 输入文件

接下来,我们将目标文件编译到共享库中:

$ clang++ -shared -o librandom.so random.o

参数-shared用于指定应该构建共享库的标志。

注意: librandom.so称为共享库。这不是随心所欲的, 呗调用的共享库应该以lib<name>.so使它们以后正确链接(如我们在下面的链接部分中所见)。

编译和链接动态可执行文件

首先,我们将为main.cpp创建一个共享对象:

$ clang++ -o main.o -c main.cpp

与之前完全相同random.o

现在,我们将尝试创建一个可执行文件:

$ clang++ -o main main.o
main.o: In function `main':
main.cpp:(.text+0x10): undefined reference to `get_random_number()'
clang: error: linker command failed with exit code 1 (use -v to see invocation)

好吧,看来我们需要告诉clang我们要使用librandom.so:

$ clang++ -o main main.o -lrandom
/usr/bin/ld: cannot find -lrandom
clang: error: linker command failed with exit code 1 (use -v to see invocation)

注意: 我们选择动态链接librandom.so到main。可以静态地执行此操作-并将random库中的所有符号直接加载到main可执行文件中。

我们告诉编译器我们要使用librandom文件。由于它是动态加载的,为什么我们在编译时需要它?好吧,原因是我们需要确保依赖的库包含可执行文件所需的所有符号。还要注意,我们指定random的是库的名称,而不是librandom.so。还记得关于库文件命名的约定吗?这是使用它的地方。

因此,我们需要让我们clang知道在哪里搜索共享库。我们用-L参数来做到这一点。请注意,由指定的路径-L仅在链接时影响搜索路径,而不会在运行时影响。我们将指定当前目录:

$ clang++ -o main main.o -lrandom -L.

现在它可以运行了,但是:

$ ./main 
./main: error while loading shared libraries: librandom.so: cannot open shared object file: No such file or directory

当找不到依赖项时,这是我们得到的错误。这将在我们的应用程序甚至运行一行代码之前发生,因为共享库是在可执行文件中的符号之前加载的。

到这就需要面对如下几个问题:

  1. main它怎么知道依赖librandom.so?

  2. main在哪里查找librandom.so?

  3. 要这么告诉main在当前目录查找librandom.so?

要回答这些问题,我们将不得不更深入地研究这些文件的结构。

ELF - 可执行和可链接的格式

共享库和可执行文件格式称为ELF(可执行和可链接格式)。如果您查看Wikipedia文章,您会发现它是一团糟,因此我们不会一一列举。总之,ELF文件包含:

  • ELF Header

  • 文件数据,可能包含:

  1. 程序header表(段头列表)

  2. 段头表(列表章节标题)

  3. 以上两个标题指向的数据

ELF标头指定程序标头表中段的大小和数量,以及节标头表中段的大小和数量。每个这样的表都由固定大小的条目组成(我使用该条目在适当的表中描述段标题或节标题)。条目是标题,并且包含指向该段或节的实际主体位置的指针(文件中的偏移量)。该主体存在于文件的数据部分中。更复杂的是-每个部分都是一个段的一部分,一个段可以包含许多段。

实际上,相同的数据要么作为段的一部分引用,要么作为段的一部分引用,这取决于当前上下文。链接时使用分段,执行时使用分段。

我们将使用readelf命令读取ELF。让我们从查看以下内容的ELF标头开始分析main

$ readelf -h main
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x4005e0
  Start of program headers:          64 (bytes into file)
  Start of p headers:          4584 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of p headers:           64 (bytes)
  Number of p headers:         30
  Section header string table index: 27

我们可以看到,这是Unix上的ELF文件(64位), 其类型为EXEC,这是一个可执行文件-符合预期。它有9个程序标头(意味着有9个segment)和30个节标头(即p)。

下一步-程序头(program headers):

$ readelf -l main

Elf file type is EXEC (Executable file)
Entry point 0x4005e0
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x000000000000089c 0x000000000000089c  R E    200000
  LOAD           0x0000000000000dd0 0x0000000000600dd0 0x0000000000600dd0
                 0x0000000000000270 0x0000000000000278  RW     200000
  DYNAMIC        0x0000000000000de8 0x0000000000600de8 0x0000000000600de8
                 0x0000000000000210 0x0000000000000210  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x0000000000000774 0x0000000000400774 0x0000000000400774
                 0x0000000000000034 0x0000000000000034  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000dd0 0x0000000000600dd0 0x0000000000600dd0
                 0x0000000000000230 0x0000000000000230  R      1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got 

同样,我们看到我们有9个程序标头。它们的类型LOAD(有2个),DYNAMIC,NOTE等等。我们也可以看到各段的部分所有权。

最后-章节标题(p headers):

$ readelf -S main
There are 30 p headers, starting at offset 0x11e8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4

  [..]

  [21] .dynamic          DYNAMIC          0000000000600de8  00000de8
       0000000000000210  0000000000000010  WA       6     0     8

  [..]

  [28] .symtab           SYMTAB           0000000000000000  00001968
       0000000000000618  0000000000000018          29    45     8
  [29] .strtab           STRTAB           0000000000000000  00001f80
       000000000000023d  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

为了简洁起见,我对此进行了修剪。我们看到列出的30个部分带有各种名称(例如.note.ABI-tag)和类型(例如SYMTAB)。

您现在可能会感到困惑, 不用担心一般不会考这方面的东西。在他们的:因为我们感兴趣的是这个文件的特定部分,我解释这个程序头表,ELF文件可以有(和共享特别库必须具有)段头一个描述段型的PT_DYNAMIC。该部分拥有一个名为的部分.dynamic,其中包含有用的信息以了解动态依赖性。

直接依赖

我们可以使用readelf实用工具来进一步探索.dynamic可执行文件的部分。

特别是,本节包含我们ELF文件的所有动态依赖项。我们仅将其指定librandom.so为依赖项,因此我们希望列出main的依赖项:

$ readelf -d main | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [librandom.so]
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

objdump可执行文件可以提供类似的结果。在这种情况下,例如:objdump -p librandom.so | grep NEEDED将打印非常相似的输出。

我们可以看到librandom.so我们指定的,但是我们还得到了四个我们没有想到的额外依赖项。这些依赖性似乎出现在所有已编译的共享库中。这些是什么呢?

  • libstdc++: 标准C++库

  • libm: 包含基本数学函数的库

  • libgcc_s: GCC(GNU编译器集合)运行时库

  • libc: C库:它定义了系统调用和其他基础设施如库open,malloc,printf,exit等。

好的, 我们已经知道main依赖于librandom.so, 那么,为什么在运行时main找不到librandom.so

运行时搜索路径

ldd是一个工具,使我们可以查看递归共享库的依赖关系。这意味着我们可以看到程序在运行时需要的所有共享库的完整列表。这也让我们看到了在那里这些依赖所在。让我们继续运行main,看看会发生什么:

$ ldd main
 linux-vdso.so.1 =>  (0x00007fff889bd000)
 librandom.so => not found
 libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f07c55c5000)
 libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f07c52bf000)
 libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f07c50a9000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f07c4ce4000)
 /lib64/ld-linux-x86-64.so.2 (0x00007f07c58c9000)

如上,我们看到了文件librandom.so依赖的动态链接库文件,但是提示是not found

我们还可以看到,我们还有两个附加的库(vdsold-linux-x86-64)。它们是间接依赖关系, 更重要的是,我们看到ldd报告了库的位置。比如libstdc++ldd报告其位置为/usr/lib/x86_64-linux-gnu/libstdc++.so.6, 这是怎么知道的呢?

我们的依赖项中的每个共享库都按顺序在以下位置进行搜索:

  1. 可执行文件rpath中列出的目录;

  2. LD_LIBRARY_PATH环境变量中的目录,该变量包含以冒号分隔的目录列表(例如:/path/to/libdir:/another/path);

  3. 可执行文件runpath中列出的目录;

  4. 缓存文件/etc/ld.so.cache和文件/etc/ld.so.conf中包含的文件目录列表;

  5. 默认系统库-通常为/lib/usr/lib (设置-z nodefaultlib参数编译时可跳过)

修复我们的可执行文件

好的, 我们验证了librandom.so是列出的依赖项,但找不到。我们知道在哪里搜索依赖项,ldd再次使用以下命令,确保目录实际上不在搜索路径中:

$ LD_DEBUG=libs ldd main
      [..]

      3650: find library=librandom.so [0]; searching
      3650:  search cache=/etc/ld.so.cache
      3650:  search path=/lib/x86_64-linux-gnu/tls/x86_64:/lib/x86_64-linux-gnu/tls:/lib/x86_64-linux-gnu/x86_64:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu/tls/x86_64:/usr/lib/x86_64-linux-gnu/tls:/usr/lib/x86_64-linux-gnu/x86_64:/usr/lib/x86_64-linux-gnu:/lib/tls/x86_64:/lib/tls:/lib/x86_64:/lib:/usr/lib/tls/x86_64:/usr/lib/tls:/usr/lib/x86_64:/usr/lib  (system search path)
      3650:   trying file=/lib/x86_64-linux-gnu/tls/x86_64/librandom.so
      3650:   trying file=/lib/x86_64-linux-gnu/tls/librandom.so
      3650:   trying file=/lib/x86_64-linux-gnu/x86_64/librandom.so
      3650:   trying file=/lib/x86_64-linux-gnu/librandom.so
      3650:   trying file=/usr/lib/x86_64-linux-gnu/tls/x86_64/librandom.so
      3650:   trying file=/usr/lib/x86_64-linux-gnu/tls/librandom.so
      3650:   trying file=/usr/lib/x86_64-linux-gnu/x86_64/librandom.so
      3650:   trying file=/usr/lib/x86_64-linux-gnu/librandom.so
      3650:   trying file=/lib/tls/x86_64/librandom.so
      3650:   trying file=/lib/tls/librandom.so
      3650:   trying file=/lib/x86_64/librandom.so
      3650:   trying file=/lib/librandom.so
      3650:   trying file=/usr/lib/tls/x86_64/librandom.so
      3650:   trying file=/usr/lib/tls/librandom.so
      3650:   trying file=/usr/lib/x86_64/librandom.so
      3650:   trying file=/usr/lib/librandom.so

      [..]

我剪裁了输出。难怪找不到我们的共享库-所在目录librandom.so不在搜索路径中!解决此问题的最特别的方法是使用LD_LIBRARY_PATH

$ LD_LIBRARY_PATH=. ./main

它可以工作,但不是很轻便。我们不想每次运行程序时都指定lib目录。更好的方法是将依赖项放入文件中, 这就需要设置rpathrunpath

rpath和runpath

rpath并且runpath是我们的运行时搜索路径“清单”中最复杂的项目。可执行文件或共享库的rpath和runpath在.dynamic我们前面介绍的部分中是可选条目。它们都是要搜索的目录列表。

rpath的类型为DT_RPATH, runpath的类型为DT_RUNPATH

rpathrunpath之间的唯一区别是搜索它们的顺序。具体来说,它们与LD_LIBRARY_PATH的顺序: rpath在LD_LIBRARY_PATH之前搜索,而runpath在LD_LIBRARY_PATH之后搜索。这意味着rpath不能用环境变量动态改变,而runpath可以。

设置rpath,看看是否可以让main工作:

$ clang++ -o main main.o -lrandom -L. -Wl,-rpath,.

参数-Wl-rpath逗号分隔将.标志传递给链接器。要进行设置runpath,我们还必须通过--enable-new-dtags参数设置(-Wl,--enable-new-dtags,-rpath,.)。让我们检查一下结果:

$ readelf -d main | grep path
 0x000000000000000f (RPATH)              Library rpath: [.]

$ ./main

可执行文件可以运行,但是已将其添加.rpath当前的工作目录中。这意味着它将无法从其他目录运行:

$ cd /tmp
$ ~/code/shared_lib_demo/main
/home/nurdok/code/shared_lib_demo/main: error while loading shared libraries: librandom.so: cannot open shared object file: No such file or directory

我们有几种解决方法。最简单的方法是复制librandom.so到搜索路径中的目录(例如/lib)。显然,更复杂的方法是我们要执行的操作-指定rpath相对于可执行文件的位置。

$ORIGIN

rpath和runpath中的路径可以是相对于当前工作目录的绝对路径(例如/path/to/my/libs/),但它们也可以是相对于可执行文件的。这是通过使用rpath定义中的$ORIGIN变量来实现的:

$ clang++ -o main main.o -lrandom -L. -Wl,-rpath,"\$ORIGIN" 

注意,$ORIGIN不是一个环境变量。如果你设置ORIGIN=/path,它将不起作用。它总是放置可执行文件的目录。

请注意,我们需要对美元符号进行转义(或使用单引号),以便我们的shell不会尝试对其进行扩展。结果是main可以在每个目录下工作并librandom.so正确找到:

$ ./main
$ cd /tmp
$ ~/code/shared_lib_demo/main

让我们使用我们的工具包来确保:

$ readelf -d main | grep path
 0x000000000000000f (RPATH)              Library rpath: [$ORIGIN]

$ ldd main
 linux-vdso.so.1 =>  (0x00007ffe13dfe000)
 librandom.so => /home/nurdok/code/shared_lib_demo/./librandom.so (0x00007fbd0ce06000)
 [..]

运行时搜索目录之安全性

如果您从命令行更改了Linux用户密码,则可能使用了该passwd命令

$ passwd
Changing password for nurdok.
(current) UNIX password: 
Enter new UNIX password: 
Retype new UNIX password: 
passwd: password updated successfully

密码被哈希之后存储在受root保护的文件/etc/shadow中,所以问题来了,非root用户如何更改此文件?

答案是passwd程序设置了setuid位,你可以通过ls看到:

$ ls -l `which passwd`
-rwsr-xr-x 1 root root 39104 2009-12-06 05:35 /usr/bin/passwd
#  ^--- This means that the "setuid" bit is set for user execution.

这是s(该行的第四个字符)。设置了此权限位的所有程序均以该程序的所有者身份运行。在此示例中,用户是root(该行的第三个单词)。

这与共享库有什么关系? 我们举个例子.

现在我们在libs目录下有了librandom.so,并且我们将main程序的rpath设置为$ORIGIN/libs:

$ ls
libs  main
$ ls libs
librandom.so
$ readelf -d main | grep path
 0x000000000000000f (RPATH)              Library rpath: [$ORIGIN/libs]

正常我们是可以运行main的,但是我们给它设置setuid位,并设置属主为root:

$ sudo chown root main
$ sudo chmod a+s main
$ ./main
./main: error while loading shared libraries: librandom.so: cannot open shared object file: No such file or directory

好吧,rpath行不通。让我们尝试设置LD_LIBRARY_PATH

$ LD_LIBRARY_PATH=./libs ./main
./main: error while loading shared libraries: librandom.so: cannot open shared object file: No such file or directory

还是不行,这里发生了什么?

出于安全考虑,使用提升的权限运行可执行文件(例如,当setuidsetgid特殊功能等)的搜索路径不同于正常:LD_LIBRARY_PATH被忽略,以及任何路径rpathrunpath包含$ORIGIN

原因是使用这些搜索路径允许利用提升的特权可执行文件以as身份运行root。有关此漏洞利用的详细信息,请参见此处。

基本上,它允许您使提升特权的可执行文件加载您自己的库,该库将以root用户(或其他用户)身份运行。以root身份运行自己的代码几乎可以使您完全控制所使用的计算机。

如果您的可执行文件需要提升的特权,则需要在绝对路径中指定依赖项,或将其放置在默认位置(例如/lib)。

这里要注意的重要行为是,对于此类应用程序,ldd我们必须面对:

$ ldd main
 linux-vdso.so.1 =>  (0x00007ffc2afd2000)
 librandom.so => /home/nurdok/code/shared_lib_demo/libs/librandom.so (0x00007f1f666ca000)
 libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f1f663c6000)
 libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f1f660c0000)
 libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f1f65eaa000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1f65ae5000)
 /lib64/ld-linux-x86-64.so.2 (0x00007f1f668cc000)

ldd不在乎setuid,它会$ORIGIN在搜索我们的依赖项时扩展。在调试对setuid应用程序的依赖项时,这可能是一个陷阱。

调试备忘单

如果在运行可执行文件时遇到此错误:

$ ./main
./main: error while loading shared libraries: librandom.so: cannot open shared object file: No such file or directory 

您可以尝试执行以下操作:

  1. 找出缺少哪些依赖项ldd <executable>;

  2. 如果您不能识别它们,则可以通过运行来检查它们是否是直接依赖项readelf -d <executable> | grep NEEDED;

  3. 确保依赖项确实存在。也许您忘了编译它们或将它们移动到libs目录中?

  4. 找出使用来搜索依赖项的位置LD_DEBUG=libs ldd <executable>;

  5. 如果您需要在搜索中添加目录:

临时:将目录添加到LD_LIBRARY_PATH环境变量 嵌入文件中:将目录添加到可执行文件或共享库的目录中,rpath或runpath通过传递-Wl,-rpath,<dir>(for rpath)或-Wl,--enable-new-dtags,-rpath,<dir>(for runpath)。使用$ORIGIN相对于可执行文件的路径。

  1. 如果ldd显示没有依赖项丢失,请查看您的应用程序是否具有提升的特权。如果是这样,ldd可能会撒谎。请参阅上面的安全问题。

原文链接: https://amir.rachum.com/blog/2016/09/17/shared-libraries/#debugging-cheat-sheet

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值