在本文中,我将尝试解释在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
当找不到依赖项时,这是我们得到的错误。这将在我们的应用程序甚至运行一行代码之前发生,因为共享库是在可执行文件中的符号之前加载的。
到这就需要面对如下几个问题:
main它怎么知道依赖
librandom.so
?main在哪里查找
librandom.so
?要这么告诉main在当前目录查找
librandom.so
?
要回答这些问题,我们将不得不更深入地研究这些文件的结构。
ELF - 可执行和可链接的格式
共享库和可执行文件格式称为ELF(可执行和可链接格式)。如果您查看Wikipedia文章,您会发现它是一团糟,因此我们不会一一列举。总之,ELF文件包含:
ELF Header
文件数据,可能包含:
程序header表(段头列表)
段头表(列表章节标题)
以上两个标题指向的数据
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
。
我们还可以看到,我们还有两个附加的库(vdso
和ld-linux-x86-64
)。它们是间接依赖关系, 更重要的是,我们看到ldd
报告了库的位置。比如libstdc++
ldd报告其位置为/usr/lib/x86_64-linux-gnu/libstdc++.so.6
, 这是怎么知道的呢?
我们的依赖项中的每个共享库都按顺序在以下位置进行搜索:
可执行文件
rpath
中列出的目录;LD_LIBRARY_PATH
环境变量中的目录,该变量包含以冒号分隔的目录列表(例如:/path/to/libdir:/another/path
);可执行文件
runpath
中列出的目录;缓存文件
/etc/ld.so.cache
和文件/etc/ld.so.conf
中包含的文件目录列表;默认系统库-通常为
/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目录。更好的方法是将依赖项放入文件中, 这就需要设置rpath
和runpath
。
rpath和runpath
rpath并且runpath是我们的运行时搜索路径“清单”中最复杂的项目。可执行文件或共享库的rpath和runpath在.dynamic我们前面介绍的部分中是可选条目。它们都是要搜索的目录列表。
rpath的类型为
DT_RPATH
, runpath的类型为DT_RUNPATH
。
rpath
和runpath
之间的唯一区别是搜索它们的顺序。具体来说,它们与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
还是不行,这里发生了什么?
出于安全考虑,使用提升的权限运行可执行文件(例如,当setuid
,setgid
特殊功能等)的搜索路径不同于正常:LD_LIBRARY_PATH
被忽略,以及任何路径rpath
或runpath
包含$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
您可以尝试执行以下操作:
找出缺少哪些依赖项
ldd <executable>
;如果您不能识别它们,则可以通过运行来检查它们是否是直接依赖项
readelf -d <executable> | grep NEEDED
;确保依赖项确实存在。也许您忘了编译它们或将它们移动到libs目录中?
找出使用来搜索依赖项的位置
LD_DEBUG=libs ldd <executable>
;如果您需要在搜索中添加目录:
临时:将目录添加到LD_LIBRARY_PATH环境变量 嵌入文件中:将目录添加到可执行文件或共享库的目录中,rpath或runpath通过传递
-Wl,-rpath,<dir>
(for rpath)或-Wl,--enable-new-dtags,-rpath,<dir>
(for runpath)。使用$ORIGIN
相对于可执行文件的路径。
如果ldd显示没有依赖项丢失,请查看您的应用程序是否具有提升的特权。如果是这样,ldd可能会撒谎。请参阅上面的安全问题。
原文链接: https://amir.rachum.com/blog/2016/09/17/shared-libraries/#debugging-cheat-sheet