本文承接上一篇动态链接3
.dynamic
节是为动态链接器提供必要的链接信息(例如在RELRO
中提到的DT_FLAGS
指示动态链接器进行立即重定位),具体.dynmaic
节的格式可以参考dynamic section。下面列出一些接下来讨论动态库搜索时会用到的一些.dynamic
节的表项。
DT_SONAME
项,仅在动态库中出现,内容是一个数字表示在.dynstr
中的偏移,表示该动态库的SONAME
(接下来会解释这个的作用)DT_NEEDED
项,内容是一个数字表示在.dynstr
中的偏移,表示该目标文件依赖的动态库(依赖多少个动态库,就存在多少个DT_NEEDED
项)。DT_RUNPATH
项,内容是一个数字表示在.dynstr
中的偏移,对应的字符串指导动态链接器搜索相应的动态库,这分两部分来讨论,link editor自己是如何搜索链接时动态库的,以及link editor保存了哪些信息在动态库中,帮助dynamic linker搜索动态库。
link editor自身也需要搜索动态库。我把动态链接1中使用的例子粘贴过来。
// lostsymlib.c
// gcc -o lostsymlib.so -fPIC -shared lostsymlib.c 编译成动态库
#include
extern int non_exsit_var;
void print_non_var(){
printf("print_non_var: %d\n", non_exsit_var);
}
void print_hello(){
printf("hello, i am .so lib\n");
}
// calllostsymlib.c
// gcc -o calllost calllostsymlib.c ./lostsymlib.so 编译为执行文件
#include
#include
void print_hello();
void print_non_var();
int non_exsit_var = 1;
int main(){
print_hello();
print_non_var();
sleep(7200);
return 0;
}
这里gcc -o calllost calllostsymlib.c ./lostsymlib.so
是直接把共享库目标文件加入到编译文件中,编译器会首先在指定的目录读取该文件,如果找不到就直接报错了。查看可执行文件calllost
的.dynamic
节。
$ readelf -d calllost
Dynamic section at offset 0xda8 contains 28 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [./lostsymlib.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
...
可以看到,.dynamic
的DT_NEED
记录了该可执行文件依赖于./lostsymlib.so
,这正是我们指定的命令行参数。
更常见的方式是用-l
参数指定。如果参数指定为-lname
,那么link editor会搜索名为libname.a
或者libname.so
的库。根据stackoverflow的讨论How to print the ld(linker) search path,默认的搜索路径可以通过默认的链接器脚本查看,在我的机器上搜索路径如下
$ ld --verbose | grep SEARCH_DIR | tr -s ' ;' \\012
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu")
SEARCH_DIR("=/lib/x86_64-linux-gnu")
SEARCH_DIR("=/usr/lib/x86_64-linux-gnu")
SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64")
SEARCH_DIR("=/usr/local/lib64")
SEARCH_DIR("=/lib64")
SEARCH_DIR("=/usr/lib64")
SEARCH_DIR("=/usr/local/lib")
SEARCH_DIR("=/lib")
SEARCH_DIR("=/usr/lib")
SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64")
SEARCH_DIR("=/usr/x86_64-linux-gnu/lib")
更加直接的方式是用-L
参数指定搜索路径,根据man ld
的文档,-L
主要具体以下特性
-L
参数会影响所有的-l
参数,不论出现的顺序-L
参数的搜索路径优先于默认的搜索路径-L
参数,搜索顺序以-L
出现的顺序为准到此为止,我们差不多明白了link editor是如何寻找动态库的,接下来看看link editor在动态库中保存了哪些信息帮助dynamic linker寻找动态库。
首先与dynamic linker密切相关的自然是.dynamic
节中的DT_NEEDED
项,它记录了该目标文件依赖的动态库。在link editor生成目标文件时(可执行文件或者动态库),该目标文件每依赖一个动态库,就在目标文件的.dynamic
节中生成一个记录该动态库名字的DT_NEEDED
项。
在stackoverflow上有一个阐述链接顺序的例子,只需要看高赞回答讨论动态链接顺序的地方就好了,link editor一对参数是--as-needed
,--no-as-needed
,在--no-as-needed
的模式下,创建目标文件时,link editor会为命令行出现的每一个动态库都创建一个DT_NEEDED
项,不论该动态库是否真的需要。--as-needed
的原文解释如下
–as-needed causes a DT_NEEDED tag to only be emitted for a library that at that point in the link satisfies a non-weak undefined symbol reference from a regular object file or, if the library is not found in the DT_NEEDED lists of other needed libraries, a non-weak undefined symbol reference from another needed dynamic library. Object files or libraries appearing on the command line after the library in question do not affect whether the library is seen as needed. This is similar to the rules for extraction of object files from archives.
所以该动态库加入依赖当且仅当:该动态库解决了来自.o
文件的符号引用或者解决了当前已依赖动态库的符号引用,并且未出现在已依赖动态库的DT_NEEDED
项中。
动态库的链接顺序影响体现在,决定该共享库是否加入依赖时,仅考虑命令行中出现在该共享库之前的库或者.o
文件(所以高赞回答举的例子相当恰到好处)。
在gcc编译时加入--verbose
选项,可以看到默认情况下,gcc会向链接器传入--as-needed
参数。
接下来讨论DT_SONAME
项。之前谈到DT_NEEDED
会记录该目标文件依赖的动态库的名字,这里的名字分两种情况,如果依赖的动态库没有SONAME
,那么记录的名字指的是命令行指定的路径,如我们在第七节的例子中看到的./lostsymlib.so
,或者通过-lname
方式指定时,对应的DT_NEEDED
项记录libname.so
。
而如果对应的动态库有DT_SONAME
项,那么该动态库对应的DT_NEEDED
的条目记录的是该动态库的SONAME
。链接器参数-soname
可以用于指定动态库的SONAME
。
然后是DT_RUNPATH
项,它指定了搜索该目标文件依赖的动态库时的搜索路径。链接器参数-rpath
可以指定,多个路径用冒号分隔。
还是以第七节的动态库和目标文件为例,这次我们修改一下编译参数。动态库的编译参数改为gcc -Wl,-soname=lostsymlib -o lostsymlib.so -fPIC -shared lostsymlib.c
。编译可执行文件的参数改为gcc -Wl,-rpath=/home/ckf/test -o calllost calllostsymlib.c ./lostsymlib.so
,其中/home/ckf/test
是生成的可执行文件所在的目录。
查看生成的动态库
$ readelf -d lostsymlib.so
Dynamic section at offset 0xe08 contains 25 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000e (SONAME) Library soname: [lostsymlib]
...
$ readelf -d calllost
Dynamic section at offset 0xd98 contains 29 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [lostsymlib]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000001d (RUNPATH) Library runpath: [/home/ckf/test]
...
可以看到,DT_SONAME
记录了动态库的名字。并且在可执行文件的DT_NEEDED
项中,./lostsymlib.so
变为了lostsymlilb
。另外DT_RUNPATH
记录了加载该可执行文件所依赖的动态库时的额外搜索路径。
不过实际运行./calllost
会报错,因为找不到lostsymlib
库,我们可以在home/ckf/test
下增加一个lostsymlib
的符号链接。这样程序就能够正常运行了。
$ ln -s liblostsym.so lostsymlib
参考man ld.so
的文档,dynamic linker获得程序控制权,完成自举和初始化工作后,便开始以广度优先顺序加载动态库。首先加载可执行文件的DT_NEEDED
项记录的动态库依赖,再加载这些动态库的DT_NEEDED
依赖…
具体搜索动态库的顺序如下:首先查找环境变量LD_LIBRARY_PATH
指定的路径,然后查找DT_RUNPATH
指定的路径。然后查找/etc/ld.so.cache
指定的路径,最后是默认路径/lib
, /usr/lib
。
注意DT_RUNPATH
指定的路径不具有继承性(这点与已经弃用的DT_RPATH
不同,一篇博客谈到了这个问题),比如上面的calllost
指定了路径/home/ckf/test
,加载上lostsymlib
动态库后,加载lostsymlib
的依赖时不会再使用/home/ckf/test
(当然lostsymlib
也可以指定自己的DT_RUNPATH
)。
另外一个有意思的环境变量是LD_PRELOAD
, 该环境变量用来预先强制加载用户指定的动态库,基于在动态链接2中谈到的全局符号介入,这个可以用来对已有的库函数进行掉包。
我们稍稍修改一下之前的例子,考虑下面三个源文件
// lostsymlib.c
// gcc -o lostsymlib.so -fPIC -shared lostsymlib.c 编译成动态库
#include
int non_exsit_var = 3;
void print_non_var(){
printf("print_non_var: %d\n", non_exsit_var);
}
void print_hello(){
printf("hello, i am .so lib\n");
}
// calllostsymlib.c
// gcc -fPIC -shared -o preload.so calllostsymlib.c 编译为动态库
#include
#include
void print_non_var();
int non_exsit_var = 1;
int aaaaa = 3;
void print_hello(){
print_non_var();
printf("aaaaa=%d\n", aaaaa);
}
// mainsym.c
// gcc -o main mainsym.o ./lostsymlib.so 编译为执行文件
void print_hello();
int main(){
print_hello();
return 0;
}
用下面两种方法运行main
$ ./main
hello, i am .so lib
$ LD_PRELOAD=/home/ckf/test/preload.so ./main
print_non_var: 1
aaaaa=3
直接运行./main
时,只有lostsymlib.so
会被加载,print_hello
便绑定到了lostsymlib.so
的函数上。但是如果强制预先加载preload.so
,根据全局符号介入的规则,print_hello
便绑定到了preload.so
的符号中。另外,preload.so
也覆盖了符号non_exsit_var
,同时使用了lostsymlib.so
中的符号print_non_var
进行重定位。
下一篇是最后一篇,动态链接5