动态链接4 程序员的自我修养第八章笔记

0. 序

本文承接上一篇动态链接3

6. .dynamic section

.dynamic节是为动态链接器提供必要的链接信息(例如在RELRO中提到的DT_FLAGS指示动态链接器进行立即重定位),具体.dynmaic节的格式可以参考dynamic section。下面列出一些接下来讨论动态库搜索时会用到的一些.dynamic节的表项。

  • DT_SONAME项,仅在动态库中出现,内容是一个数字表示在.dynstr中的偏移,表示该动态库的SONAME(接下来会解释这个的作用)
  • DT_NEEDED项,内容是一个数字表示在.dynstr中的偏移,表示该目标文件依赖的动态库(依赖多少个动态库,就存在多少个DT_NEEDED项)。
  • DT_RUNPATH项,内容是一个数字表示在.dynstr中的偏移,对应的字符串指导动态链接器搜索相应的动态库,这

7. link editor与动态库搜索

分两部分来讨论,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]
 ...

可以看到,.dynamicDT_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寻找动态库。

8. link editor相关的参数与保存信息

首先与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

9. dynamic linker的动态库搜索

参考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

你可能感兴趣的:(C与C++,编译,链接,C)