动态链接(二)

5. 动态链接相关结构

首先装载方面和静态链接下的装载基本无异,唯一不同的是装载完之后控制权交给动态链接器,而不是可执行文件的入口。系统加载完动态链接器之后将控制权交给动态链接器的入口地址,接着动态链接器进行一系列的初始化及链接工作,完成之后将控制权交给可执行文件的入口,开始执行。

5.1 .interp段

在动态链接的ELF可执行文件中,存在.interp段,专门说明需要用到的动态链接器的路径:

动态链接(二)_第1张图片

这路径实际上是一个软链,因此当链接器版本更新时,这里并不需要修改。也可以使用以下命令查看一个可执行文件所需要的动态链接器的路径:

5.2 .dynamic段

该段保存了动态链接器需要的基本信息,比如依赖于哪些共享对象,动态链接符号表的位置,动态链接重定位表的位置等。里面保存的是一个结构体数组,32位的结构如下:

typedef struct {
	Elf32_Sword d_tag;
	union {
		Elf32_Word d_val;
		Elf32_Addr d_ptr;
	}d_un;
}Elf32_Dyn;

由一个类型加上附加的数值或指针组成,常见的类型如下:

动态链接(二)_第2张图片

有点类似于ELF文件头,ELF文件头保存的是静态链接下相关的内容,而这里保存的是动态链接下相关的内容。

使用readelf -d Lib.so可以查看.dynamic的内容。Linux还提供了一个命令用来查看一个程序主模块或一个共享库依赖于哪些共享库:

5.3 动态符号表

一般情况下,假设模块A引用了模块B的一个函数,称模块A导入了该函数,模块B导出了该函数。ELF专门有一个动态符号表来保存这些信息,为.dynsym。一般动态链接的模块同时拥有.dynsym和.symtab两个表,.symtab中保存了所有符号,.dynsym只保存与动态链接相关的符号,.symtab包括了.dynsym。还有一些辅助的表,如动态符号字符串表.dynstr和便于加快符号查找过程的符号哈希表.hash。

5.4 动态链接重定位表

在静态链接中,目标文件里有用于表示重定位信息的重定位表,如.rel.text表示段码段的重定位表,.rel.data表示数据段的重定位表。而动态链接中,也有类似的重定位表.rel.dyn和.rel.plt,分别相当于.rel.text和.rel.data。.rel.dyn实际上是对数据引用的修正,所修正的位置位于.got及数据段,.rel.plt是对函数引用的修正,所修正的位置位于.got.plt。可以使用readelf查看动态链接文件的重定位表:

动态链接(二)_第3张图片

可以看到有三种新的重定位类型JUMP_SLOT,GLOB_DAT和RELATIVE。

JUMP_SLOT类型位于.rel.plt中,修正方式很简单,比如上面这里的printf,动态链接器首先查找printf的地址,比如是0x08801234,链接器会将这个地址填入到.got.plt中的偏移为0x201018的位置中去,从而实现了地址的重定位。

GLOB_DAT类型也是一样,只不过是对.got的重定位。

RELATIVE类型实际上就是基址重置(Rebasing)。共享对象的数据段也有可能包含绝对地址引用,如下:

static int a;
static int *p=&a;

在编译时,共享对象的地址从0开始,假设a相对于起始地址0的偏移为B,则p的值是B,一旦共享对象被装载到地址A,那么a的地址为A+B,则p的值也要加上A。RELATIVE类型的重定位入口就是专门用来重定位p这种类型的。

5.5 动态链接时进程堆栈初始化信息

动态链接器开始链接工作时,还需要知道可执行文件和本进程的一些信息,如几个段,段的属性等。这些信息是由操作系统通过堆栈传递给动态链接器的。这些信息的格式也是一个结构数组,32位的结构如下:

typedef struct {
	unit32_t a_type;
	union {
		unit32_t a_val;
	}a_un;
}Elf32_auxv_t;

结构和前面.dynamic里的结构很类似,常见的几个类型如下:

动态链接(二)_第4张图片

动态链接(二)_第5张图片

它在堆栈中的位于环境变量指针的后面,如下:

动态链接(二)_第6张图片

可以写个程序把堆栈中初始化的信息全部打印出来,64位下的源码:

#include
#include

int main(int argc,char *argv[]) {
	long long *p=(long long  *)argv;
	int i;
	Elf64_auxv_t *aux;
	printf("Argument count:%lld\n",*(p-1));

	for(i=0;i<*(p-1);i++) 
		printf("Argument %d:%s\n",i,*(p+i));

	p+=i;
	p++;

	printf("Environment:\n");
	while(*p) {
		printf("%s\n",*p);
		p++;
	}

	p++;

	//printf("%d\n",sizeof(Elf64_auxv_t));
	printf("Axuilliary Vector:\n");
	aux=(Elf64_auxv_t *)p;
	while(aux->a_type!=AT_NULL) {
		printf("Type:%lld Value: %lld\n",aux->a_type,aux->a_un.a_val);
		aux++;
	}

	return 0;
}

6. 动态链接的步骤和实现

主要分三步:(1)启动动态链接器本身。(2)装载所需要的共享对象。(3)重定位和初始化。

6.1 动态链接器自举

动态链接器不能再依赖于其他共享对象,并且自身的重定位工作由自身完成,这个过程称为自举。自举代码首先会找到自己的GOT,GOT中第一项保存的即是.dynamic的偏移,由此找到了.dynamic,通过.dynamic中的信息,可以获得动态链接器本身的重定位表和符号表等,从而得到动态链接器本身的重定位入口,并将它们重定位。完成之后,动态链接器代码中才可以使用自己的全局变量和静态变量。

实际上,在自举完成之前,除了不能使用全局和静态变量,连内部函数也不能调用。前面提到过内部函数调用可以使用相对地址调用,但实际上存在一个全局符号介入的问题(后面会介绍),因此在PIC模式编译下的共享对象,内部函数调用也是采用模块外部函数调用一样的方式,即使用GOT/PLT的方式。因此在GOT/PLT没有被重定位之前,不能调用函数。

6.2 装载共享对象

完成自举后,动态链接器将可执行文件和链接器本身的符号表合并到一个全局符号表中。在.dynamic中类型为DT_NEEDED的即为该模块所依赖的共享对象。动态链接器查看可执行文件的.dynamic,将可执行文件所需要的所有共享对象放入一个集合中。然后从该集合中取一个所需要的共享对象的名字,找到相应的文件,将代码段和数据段映射到进程空间中,并且查看.dynamic将该共享对象所依赖的共享对象继续放入集合中,如此循环。装载顺序可以使用广度优先和深度优先,一般是广度优先。

当一个共享对象被装载进来时,它的符号表都会合并到全局符号表中。

以下是一个涉及到符号优先级的例子:

//a1.c
#include

void a() {
	printf("a1.c\n");
}

//a2.c
#include

void a() {
	printf("a2.c\n");
}

//b1.c
void a();

void b1() {
	a();
}

//b2.c
void a();

void b2() {
	a();
}

在编译时指定依赖关系,如b1.so依赖于a1.so,b2.so依赖于a2.so:

gcc -fPIC -shared a1.c -o a1.so
gcc -fPIC -shared a2.c -o a2.so
gcc -fPIC -shared b1.c a1.so -o b1.so
gcc -fPIC -shared b2.c a2.so -o b2.so

再有一个main:

#include

void b1();
void b2();

int main() {
	b1();
	b2();
	return 0;
}

编译命令如下,-XLinker -rpath ./表示链接器在当前路径寻找共享对象:

gcc main.c b1.so b2.so -o main -XLinker -rpath ./

运行main:

不是所预期的a1.c和a2.c。实际上这就是共享对象全局符号介入的问题。按照广度优先顺序装载的话,装载顺序为b1.so,b2.so,a1.so,a2.so。装载完a1.so之后,符号a已经在全局符号表中存在了,当装载a2.so时候,发现同名符号a在全局符号表中存在了,便忽略掉了a2.so中的a。因此b1和b2中对a的引用都重定位到了a1.so中的a。

因此前面提到的内部函数调用,假设模块A调用内部函数fun,如果使用相对地址调用,那么当fun在全局符号表中被同名符号覆盖时,模块A中调用fun处的指令的相对地址那部分需要重定位,这又与共享对象的地址无关性矛盾。因此内部函数调用还是使用GOT/PLT的方式,当fun被覆盖时,只需要重定位.got.plt,不影响代码段的地址无关性。

如果将函数定义为static,代表是该模块私有的,那么则可以使用相对地址调用。

6.3 重定位和初始化

上面步骤完成后,链接器开始重新遍历可执行文件和每个共享对象的重定位表,将它们的GOT/PLT中的每个需要重定位的位置进行修正。重定位完成之后,对于有些共享对象有.init段和.finit段,动态链接器还会执行这两个段的代码(可执行文件的.init和.finit不是由动态链接器执行)。最后,把进程的控制权交给程序的入口。

7. 显示运行时链接

显示运行时链接也叫运行时加载,就是让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。这种共享对象被叫做动态装载库(Dynamic Loading Library)。

主要的优势有:

(1)不必一开始就把所有共享对象全部装载进来,减少程序启动时间和内存使用。

(2)程序本身不必重新启动而实现模块的增加,删除和更新等。

从文件格式上看,动态库跟一般的共享对象没有区别。对于一般共享对象,是由动态链接器负责装载和链接的,对程序本身是透明的。而动态库的装载是程序通过一系列动态链接器提供的API来操作的。主要有4个函数:(1)打开动态库dlopen。(2)查找符号(dlsym)。(3)错误处理(dlerror)。(4)关闭动态库(dlclose)。

7.1 dlopen()

dlopen()用来打开一个动态库,并将其加载到进程的地址空间,完成初始化过程,原型如下:

void *dlopen(const char *filename, int flag);

第一个参数是动态库的路径,如果是相对路径,dlopen()会按一定的顺序去不同的地方寻找该动态库。如果该参数设为0,dlopen返回的将是全局符号表的句柄。全局符号表包括了可执行文件本身,被动态链接器加载的共享对象以及运行时通过dlopen打开并且使用了RTLD_GLOBAL方式的模块中的符号。

第二个参数是符号的解析方式,RTLD_LAZY表示使用延迟绑定,RTLD_NOW表示当模块加载时一次性完成所有的函数绑定工作,如果有任何未定义的符号引用的绑定工作没法完成,dlopen()就返回错误。另外还有RTLD_GLOBAL可以和上面两个作“或”运算一起使用,表示将被加载的模块的全局符号合并到进程的全局符号表中。

dlopen返回的是被加载模块的句柄,加载失败则返回NULL。如果之前已被加载过,则返回同一个句柄。另外对于有依赖关系的,需要手动加载。如果A依赖B,则需先手动加载B,在加载A。另外在完成加载时,dlopen也会执行模块的.init段的代码再返回。

7.2 dlsym()

可以通过这个函数找到所需要的符号,原型如下:

void *dlsym(void *handle, char *symbol);

第一个参数是句柄,第二个参数是要查找的符号的名字。如果找到该符号则返回该符号的值,否则返回NULL。如果查找的符号是函数,则返回的是函数的地址,如果是个变量,返回的是变量的地址,如果是个常量,则返回该常量的值。如果符号的常量值刚好是NULL或0,可以通过dlerror()判断是否找到了。

前面提到过先装载的符号优先级较高,这种优先级序列称为装载序列。而dlsym()对符号的查找优先级分两种:

(1)如果handle参数是全局符号表的句柄,那么查找的优先级自然也是装载序列,因为是在全局符号表中查找。

(2)如果handle是某个通过dlopen()打开的共享对象的句柄,那么采用的是依赖序列的优先级。依赖序列是指以该句柄表示的共享对象为根节点,对它所有依赖的共享对象进行广度优先遍历,直到找到符号为止。

7.3 dlerror()

每次调用完dlopen(),dlsym()和dlclose()之后,都可以调用dlerror()判断上一次调用是否成功。返回类型是char *。返回NULL表示上一次调用成功,否则返回相应的错误信息。

7.4 dlclose()

dlclose()表示将某个已经加载的模块卸载。系统会为每个加载的模块维护一个计数器,每次使用dlopen()加载时,计数器加1,使用dlclose()时,计数器减1,当减到0时,该模块才真正卸载掉,并且会执行.finit段的代码,然后将相应的符号从符号表中去除,取消进程空间跟模块的映射关系,然后关闭模块文件。

演示以下获取sin()函数的地址并且调用它,simple.c:

#include
#include

int main(int argc,char *argv[]) {
	void *handle;
	double (*func)(double);
	char *error;

	handle=dlopen(argv[1],RTLD_NOW);
	if(handle==NULL) {
		printf("Open library %s error: %s\n",argv[1],dlerror());
		return -1;
	}

	func=dlsym(handle,"sin");
	if((error=dlerror())!=NULL) {
		printf("Symbol sin not found: %s\n",error);
	} else 
		printf("%f\n",func(3.1415926/2));

	dlclose(handle);
	
	return 0;
}

编译命令:

-ldl表示使用DL库。

你可能感兴趣的:(程序员的自我修养,Linux)