动态链接(二)

5. 动态链接相关结构

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

5.1 .interp段

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

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

5.2 .dynamic段

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

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

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

有点类似于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查看动态链接文件的重定位表:

可以看到有三种新的重定位类型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里的结构很类似,常见的几个类型如下:

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

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

#include<stdio.h>
#include<elf.h>

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<stdio.h>

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

//a2.c
#include<stdio.h>

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<stdio.h>

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<stdio.h>
#include<dlfcn.h>

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库。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一、 课程设计目的 学会用C++语言和数据结构知识实现表达式的解析与计算;学会使用动态链接库技术进行编程;学会编辑、编译、运行MFC应用程序的基本过程.学会MFC可视化编程技术。 、 课程设计内容与实现的功能 1.C++语言的顺序结构,分支结构,循环结构,函数,结构体,指针,MFC可视化编程技术。 2.数据结构中的叉树数据组织、存贮、后序遍历及其操作。 3. 使用动态链接库进行函数模块的设计,实现计算功能。 4.编程中使用了动态连接库技术; 能实现表达式的输入,解析与计算 三、 系统分析与设计 1、系统分析 应用程序使用DLL可以采用两种方式:一种是隐式链接,另一种是显式链接。在使用DLL之前首先要知道DLL中函数的结构信息。Visual C++6.0在VC in目录下提供了一个名为Dumpbin.exe的小程序,用它可以查看DLL文件中的函数结构。另外,Windows系统将遵循下面的搜索顺序来定位DLL: 1.包含EXE文件的目录,2.进程的当前工作目录, 3.Windows系统目录, 4.Windows目录,5.列在Path环境变量中的一系列目录。 1.隐式链接 隐式链接就是在程序开始执行时就将DLL文件加载到应用程序当中。实现隐式链接很容易,只要将导入函数关键字_declspec(dllimport)函数名等写到应用程序相应的头文件中就可以了 2.显式链接 显式链接是应用程序在执行过程中随时可以加载DLL文件,也可以随时卸载DLL文件,这是隐式链接所无法作到的,所以显式链接具有更好的灵活性,对于解释性语言更为合适。不过实现显式链接要麻烦一些。在应用程序中用 LoadLibrary或MFC提供的AfxLoadLibrary显式的将自己所做的动态链接库调进来,动态链接库的文件名即是上述两个函数的参数,此后再用GetProcAddress()获取想要引入的函数。自此,你就可以象使用如同在应用程序自定义的函数一样来调用此引入函数了。在应用程序退出之前,应该用FreeLibrary或MFC提供的AfxFreeLibrary释放动态链接库。 2、系统设计: 按照系统设计要求,用Visual C++设计和开发一个MFC应用程序---高级计算器。计算器的界面设计、计算功能实现、算式计算和动态链接库的使用等。提交由需求分析:系统设计说明、系统技术文档、系统使用说明书和源程序代码为附录构成的实验报告。 2.1、模块设计: 1. 计算器界面 计算器界面包括主题“高级计算器”和背景图片以及电子日历。在计算区上有显示文本框和数字按钮以及运算符按钮。在计算区上部分有运算模式选择模块和小提示语句。 2. 模块功能简介 系统分为多个模块,分别为计算模块、错误分析模块、界面显示模块和模式选择模块。其中计算模块用来进行各种基本的加、减、乘、除的运算并且显示运算结果;错误分析模块用来进行错误处理;界面显示模块用来美化计算器界面;模式选择模块用来进行运算模式的选择和切换,以实现不同的运算要求
要安装动态链接库,首先需要确定你的系统是基于哪个 Linux 发行版。一般情况下,Linux 发行版都会有自己的包管理器来管理软件包,你可以使用包管理器来安装动态链接库。 以 Debian/Ubuntu 发行版为例,你可以使用以下命令来安装动态链接库: ``` sudo apt-get update sudo apt-get install 库名 ``` 其中,“库名”是指你要安装的动态链接库的名称。如果你想安装多个动态链接库,可以将它们依次列在一行中,用空格分隔开。 如果你的系统不是基于 Debian/Ubuntu,那么可以通过其他方式来安装动态链接库。例如,你可以从官方网站下载动态链接库的进制文件,然后按照说明进行安装。 无论你使用哪种方式安装动态链接库,都需要确保它们被正确地安装到了系统路径中。通常情况下,动态链接库的安装路径是 /usr/lib 或 /usr/local/lib。如果你的应用程序需要使用这些动态链接库,那么你需要将这些路径添加到 LD_LIBRARY_PATH 环境变量中。 例如,假设你安装了名为 libfoo.so 的动态链接库,它的安装路径是 /usr/lib,那么你可以按照以下方式设置 LD_LIBRARY_PATH 环境变量: ``` export LD_LIBRARY_PATH=/usr/lib:$LD_LIBRARY_PATH ``` 这将告诉系统在 /usr/lib 目录中查找动态链接库。如果你安装了多个动态链接库,那么需要将它们全部添加到 LD_LIBRARY_PATH 环境变量中,用冒号分隔开。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值