第7章链接:如何动态连接共享库、从应用程序中加载和链接共享库

文章目录

  • 7.10 动态链接共享库
    • 静态库的缺点
    • 何为共享库
    • 共享库的"共享"的含义
    • 动态链接过程
  • 7.11 从应用程序中加载和链接共享库
    • 运行时动态加载和连接共享库的接口 dlopen
    • 函数 dlsym
    • 函数 dlclose
    • 函数 dlerror
    • 动态加载和链接共享库的应用程序示例
  • 7.12 *与位置无关的代码(PIC)
    • 7.12.1 PIC 数据引用
    • 7.12.2 PIC 函数调用

7.10 动态链接共享库

静态库的缺点

  1. 和所有的软件一样,需要定期维护和更新。如果应用程序员想要使用一个库的最新版本,他们必须以某种方式了解到该库的更新情况,然后显式地将它们的程序与新的库重新链接。
  2. 几乎每个 C 程序都使用标准 I/O 函数,比如 prinfscanf。在运行时,这些函数的代码会被复制到每个运行进程的文本段中。在一个运行 50 ~ 100 个进程的典型系统中,这会是对稀少的存储器资源的极大浪费。(存储器的一个有趣属性就是不论一个系统中有多大的存储器,它总是一种稀有的资源。磁盘空间和厨房的垃圾桶同样有这种属性。)

何为共享库

共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并在存储器中和一个程序链接起来。这个过程称为动态链接(dynamic linking),是由一个叫做动态链接器(dynamic linker)的程序来执行的。

共享库也称为共享目标(shared object),在 Unix 系统中通常用 .so 后缀来表示。微软的操作系统大量地利用了共享库,它们称为 DLL (动态链接库)。

共享库的"共享"的含义

共享库的“共享” 在两个方面有所不同。

  • 首先,在任何给定的文件系统中,对于一个库只有一个 .so 文件。所有引用该库的可执行目标文件共享这个 .so 文件中的代码和数据,而不是像静态库的内容那样被拷贝和嵌入到引用它们的可执行的文件中。
  • 其次,在存储器中,一个共享库的 .text 节只有一个副本可以被不同的正在运行的进程共享。

动态链接过程

下图是如下程序的动态链接过程:
在这里插入图片描述
在这里插入图片描述
为了构造图7.5 中向量运算示例程序的共享库 libvector.so,会调用编译器,给链接器如下特殊指令:

# -fPIC 选项指示编译器生成与位置无关的代码
# -shared 选项指示链接器创建一个共享的目标文件
unix> gcc -shared -fPIC -o libvector.so addvec.c multvec.c

一旦创建了这个库,随后就要将它链接到图 7.6 的示例程序中。

unix> gcc -o p2 main2.c ./libvector.so

这样就创建了一个可执行目标文件 p2,而此文件的形式使得它在运行时可以和 libvector.so 链接。

基本思路是当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程。

认识到这一点是很重要的:在此时刻,没有任何 libvector.so 的代码和数据节被真的拷贝到可执行文件 p2 中。取而代之的是,链接器拷贝了一些重定位和符号表信息,它们使得运行时可以解析对 libvector.so 中代码和数据的引用。

当加载器加载和运行可执行文件 p2 时,它利用 7.9 节讨论过的技术,加载部分链接的可执行文件 p2。

接着,它注意到 p2 包含一个 .interp 节,这个节中包含动态链接器的路径名,动态链接器本身就是一个共享目标(比如,在 Linux 系统上的 LD-LINUX.SO)。加载器不再像它通常那样将控制传递给应用,取而代之的是加载和运行这个动态链接器。

然后,动态链接器通过执行下面的重定位完成链接任务:

  • 重定位 libc.so 的文本和数据到某个存储器段。在 IA32/Linux 系统中,共享库被加载到从地址 0x40000000 开始的区域中(见第7章链接:重定位、可执行目标文件、加载可执行目标文件中的图7.13)
  • 重定位 libvector.so 的文本和数据到另一个存储器段。
  • 重定位 p2 中所有对由 libc.solibvector.so 定义的符号的引用。

最后,动态链接器将控制传递给应用程序。从这个时刻开始,共享库的位置就固定了,并且在程序执行的过程中都不会改变。

7.11 从应用程序中加载和链接共享库

到此刻为止,已经讨论了在应用程序执行之前,即应用程序被加载时,动态链接器加载和链接共享库的情景。然而,应用程序还可能在它运行时要求动态链接器加载和链接任意共享库,而无需在编译时链接那些库到应用中。

动态链接是一项强大有用的技术。下面是一些现实的例子:

  • 分发软件。微软 Windows 应用的开发者常常利用共享库来分发软件更新。他们生成一个共享库的版本,然后用户可以下载,并用它替代当前的版本。下一次他们运行应用程序时,应用将自动链接和加载新的共享库。
  • 构建高性能 Web 服务器。许多Web服务器生成动态内容,比如个性化的 Web 页面、账户余额和广告标语。早期的 Web 服务器通过使用 forkexecve 创建一个子进程,并在该子进程的上下文中运行 CGI 程序,来生成动态内容。然而,现代高性能的 Web 服务器可以使用基于动态链接的更有效和完善的方法来生成动态内容。

其思路是将生成动态内容的每个函数打包在共享库中。当一个来自 Web 浏览器的请求到达时,服务器动态地加载和链接适当的函数,然后直接调用它,而不是使用 forkexecve 在子进程的上下文中运行函数。函数会一直缓存在服务器的地址空间中,所以只要一个简单的函数调用的开销就可以处理随后的请求了。这对一个繁忙的网站来说是有很大影响的。更进一步,可以在运行时,无需停止服务器,更新已存在的函数,以及添加新的函数。

运行时动态加载和连接共享库的接口 dlopen

像 Linux 和 Solaris 这样的 Unix 系统,为动态链接器提供了一个简单的接口,允许应用程序在运行时加载和链接共享库。

#include <dlfcn.h>

//返回:若成功则为指向句柄的指针,若出错则为Null
void *dlopen(const char *filename, int flag);
  • dlopen 函数加载和链接共享库 filename。用以前带 RTLD_GLOBAL 选项打开的库解析 filename 中的外部符号。如果当前可执行文件是带 -rdynamic 选项编译的,那么对符号解析而言,它的全局符号也是可用的。

  • flag 参数必须要么包括 RTLD_NOW,该标志告诉链接器立即解析对外部符号的引用,要么包括 RTLD_LAZY 标志,该标志指示链接器推迟符号解析直到指向来自库中的代码时。这两个值中的任意一个都可以和 RTLD_GLOBAL 标志取或。

函数 dlsym

#include <dlfcn.h>

//返回:若成功则为指向符号的指针,若出错则为Null
void *dlsym(void *handle, char *symbol);

dlsym 函数的输入是一个指向前面已经打开共享库的句柄和一个符号名字,如果该符号存在,就返回符号地址,否则返回 NULL。

函数 dlclose

#include <dlfcn.h>

//返回:若成功则为0,若出错则为1
int dlclose(void *handle);

如果没有其他共享库还在使用这个共享库,dlclose 函数就卸载该共享库。

函数 dlerror

#include <dlfcn.h>

//返回:如果前面对dlopen、dlsym 或 dlclose 的调用失败,则为错误消息,如果前面的调用成功,则为NULL
const char *dlerror(void);

dlerror 函数返回一个字符串,它描述的是调用 dlopendlsym 或者 dlclose 函数时发生的最近的错误,如果没有错误发生,就返回 NULL。

动态加载和链接共享库的应用程序示例

下面的程序展示了如何利用这个接口动态链接到 libvector.so 共享库,然后调用它的 addvec 程序。

//dll.c
//一个动态加载和链接共享库 libvector.so 的应用程序
#include <stdio.h>
#include <dlfcn.h>

int x[2] = {1, 2};
int y[2] = {3, 4};

int z[2];

int main()
{
	void *handle;
	void (*addvec)(int *, int *, int *, int);
	char *error;

	/* dynamically load the shared library that contains addvec() */
	handle = dlopen("./libvector.so", RTLD_LAZY);
	if (!handle) {
		fprintf(stderr, "%s\n", dlerror());
		exit(1);
	}

	/* get a pointer to the addvec() function we just loaded */
	addvec = dlsym(handle, "addvec");
	if ((error = dlerror()) != NULL)  {
		fprintf(stderr, "%s\n", error);
		exit(1);
	}
	
	/* Now we can call addvec() just like any other function */
	addvec(x, y, z, 2);
	printf("z = [%d %d]\n", z[0], z[1]);
	
	/* unload the shared library */
	if (dlclose(handle) < 0) {
		fprintf(stderr, "%s\n", dlerror());
		exit(1);
	}

	return 0;
}

要编译这个程序,将以下面的方式调用 GCC:

unix> gcc -rdynamic -O2 -o p3 main3.c -ldl

旁注:共享库和 Java 本地接口

Java 定义了一个标准调用规则,叫做 Java 本地接口(Java Native Interface,JNI),它允许 Java 程序调用 “本地的” C 和 C++ 函数。 JNI 的基本思想是将本地 C 函数,比如说 foo,编译到共享库中,比如说 foo.so。当一个正在运行的 Java 程序试图调用函数 foo 时,Java 解释程序利用 dlopen 接口(或者某个类似于此的东西)动态链接和加载 foo.so,然后再调用 foo。

7.12 *与位置无关的代码(PIC)

共享库的一个主要目的就是允许多个正在运行的进程共享存储器中相同的库代码,因而节约宝贵的存储器的资源。

那么,多个进程是如何共享一个程序的一个拷贝的呢?
1、一种方法是给每个共享库分配一个事先预备的专用的地址空间组块(chunk),然后要求加载器总是在这个地址加载共享库。虽然这种方法很简单,但是它也造成了一些严重的问题。首先,它对地址空间的使用效率不高,因为即使一个进程不使用这个库,那部分空间还是会被分配出来。第二,它也难以管理。我们将不得不保证没有组块会重叠。每次当一个库修改了之后,必须确认它的已分配的组块还适合它的大小。如果不适合了,必须找一个新的组块。并且,如果我们创建了一个新的库,我们还必须与为它寻找空间。随着时间的发展,假设在一个系统中有了成百个库和各种版本的库,就很难避免地址空间分裂成大量小的、未使用而又不再能使用的小洞。甚至更糟的是,对每个系统而言,从库到存储器的分配都是不同的,这就引起了更多令人头痛的管理问题。

2、一种更好的方法是编译库代码,使得不需要链接器修改库代码,就可以在任何地址加载和执行这些代码。这样的代码叫做与位置无关的代码(position-independent code,PIC)。用户对 GCC 使用 -fPIC 选项指示 GNU 编译系统生成 PIC 代码。

在一个IA32 系统中,对同一个目标模块中过程的调用是不需要特殊处理的,因为引用是 PC 相关的,已知偏移量,就已经是 PIC 了。然而,对外部定义的过程调用和对全局变量的引用通常不是 PIC,因为它们都要求在链接时重定位。

7.12.1 PIC 数据引用

编译器通过如下事实来生成对全局变量的 PIC 引用:无论在存储器中的何处加载一个目标模块(包括共享目标模块),数据段总是分配为紧随在代码段后面。因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对存储器位置是无关的

为了使用这个事实,编译器在数据段开始的地方创建了一个表,叫做全局偏移量表(global offset table,GOT)。GOT 包含每个被这个目标模块引用的全局数据目标的表目。编译器还为 GOT 中每个表目生成一个重定位记录。在加载时,动态链接器会重定位 GOT 中的每个表目,使得它包含正确的绝对地址。每个引用全局数据的目标模块都有一张自己的 GOT。

在运行时,使用下面的代码形式,通过 GOT 间接地引用每个全局变量:
在这里插入图片描述
在这段代码中,对 L1 的调用将返回地址(正好就是 pop1 指令的地址)压入栈中。随后,popl 指令把这个地址弹出到 %ebx 中。这两条指令的最终效果是将 PC 的值移到寄存器 %ebx 中。

指令 addl%ebx 增加一个常量偏移量,使得它指向 GOT 中适当的表目,该表目包括数据项的绝对地址。此时,就可以通过包含在 %ebx 中的 GOT 表目间接地引用全局变量了。在这个例子中,两条 movl 指令(间接地通过 GOT)加载全局变量的内容到寄存器 %eax 中。

PIC 代码有性能缺陷。现在每个全局变量引用需要五条指令而不是一条,还需要一个额外的对 GOT 的存储器的引用。而且,PIC 代码还要用一个额外的寄存器来保持 GOT 表目的地址。在具有大寄存器文件的机器上,这不是一个大问题。然而,在寄存器供应不足的 IA32 系统中,即使失掉一个寄存器也会造成寄存器溢出到栈中。

7.12.2 PIC 函数调用

PIC 代码当然可以用相同的方法来解析外部过程调用:

在这里插入图片描述
不过,这种方法对每一个运行时过程调用都要求三条额外的指令。取而代之,ELF编译系统使用一种有趣的技术,叫做延迟绑定(lazy binding),将过程地址的绑定推迟到第一次调用该过程时。第一次调用过程的运行时开销很大,但是其后的每次调用都只会花费一条指令和一个间接的存储器引用。

延迟绑定是通过两个数据结构之间简洁但又有些复杂的交互来实现的,这两个数据结构是:GOT 和 PLT(procedure linkage table,过程链接表)。如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的 GOT 和 PLT。GOT 是 .data 节的一部分,PLT是 .text 节的一部分。

下图展示了之前示例程序 main.o 的 GOT 的格式。头三条 GOT 表目是特殊的:GOT[0] 包含 .dynamic 段的地址,这个段包含了动态链接器用来绑定过程地址的信息,比如符号表的位置和重定位信息;GOT[1] 包含一些定义这个模块的信息;GOT[2] 包含动态链接器的延迟绑定代码的入口点。
在这里插入图片描述
原始代码见如下两图:
在这里插入图片描述
在这里插入图片描述
定义在共享目标中并被 main.o 调用的每个过程在 GOT 中都会有一个表目,从 GOT[3] 表目开始。对应示例程序,给出了printfaddvec 的 GOT 表目,printf 定义在 libc.so 中,而 addvec 定义在 libvector.so 中。

下图展示了示例程序 p2 的 PLT。PLT 是一个 16 字节表目的数组。第一个表目 PLT[0] 是一个特殊表目,它跳转到动态链接器中。每个被调用的过程在 PLT 中都有一个表目,从 PLT[1] 开始。在图中,PLT[1] 对应于 printf,PLT[2] 对应于 addvec

在这里插入图片描述在这里插入图片描述
初始地,在程序被动态链接并开始执行后,过程 printfaddvec 被分别绑定到它们相应的 PLT 表目中的第一条指令上。比如,对 addvec 的调用有如下形式:
在这里插入图片描述
addvec 第一次被调用时,控制转移到 PLT[2] 的第一条指令,该指令通过 GOT[4] 执行一个间接跳转。初始地,每个 GOT 表目包含相应的 PLT 表目中 pushl 表目的地址。所以,PLT 中的间接跳转仅仅是将控制转移回到 PLT[2] 中的下一条指令。这条指令将 addvec 符号的 ID 压入栈中。最后一条指令跳转到 PLT[0],从 GOT[1] 中将另外一个标识信息的字压入栈中,然后通过 GOT[2] 间接跳转到动态链接器中。动态链接器用两个栈表目来确定 addvec 的位置,用这个地址覆盖 GOT[4],并把控制传递给 addvec

下一次在程序中调用 addvec 时,控制像前面一样传递给 PLT[2]。不过这次通过 GOT[4] 的间接跳转将控制传递给 addvec。从此刻起,唯一额外的开销就是对间接跳转的存储器引用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/21240.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

强大,Midjourney Imagine API接口,AI画画的福音!

前几天跟大家分享过一篇 ”让chatGPT教你AI绘画|如何将chatGPT与Midjourney结合使用&#xff1f;“&#xff0c;但是由于许多小伙伴们使用Midjourney还有许多困难&#xff0c;又要上网&#xff0c;还要注册Discord&#xff0c;MJ的使用成本很高&#xff0c;让大家望而却步&…

链表题目强化练

目录 前言&#xff08;非题目&#xff09; 两数相加 删除链表的倒数第N个结点 环形链表 相交链表 合并 K 个升序链表 复制带随机指针的链表 前言&#xff08;非题目&#xff09; 初学者在做链表的题目时有一个特点&#xff0c;就是每看一个链表的题都觉得很简单&#x…

Python程序员职业现状分析,想提高竞争力,就要做到这六点

现今程序员群体数量已经高达几百万&#xff0c;学历和收入双高&#xff0c;月薪普遍过万。今天&#xff0c;我们就围绕90后程序员人群分析、职业现状、Python程序员分析等&#xff0c;进行较为全面的报告分析和观点论述。 一、程序员人群分析 人数规模上&#xff1a;截当前程…

【设计原则与思想:总结课】38 | 总结回顾面向对象、设计原则、编程规范、重构技巧等知识点

到今天为止&#xff0c;设计原则和思想已经全部讲完了&#xff0c;其中包括&#xff1a;面向对象、设计原则、规范与重构三个模块的内容。除此之外&#xff0c;我们还学习了贯穿整个专栏的代码质量评判标准。专栏的进度已经接近一半&#xff0c;马上就要进入设计模式内容的学习…

类似于ChatGPT的优秀应用notion

notion 是一款流行的笔记应用。不过功能实际远超笔记&#xff0c;官方自己定义是&#xff1a;“将笔记、知识库和任务管理无缝整合的协作平台”。其独特的 block 概念&#xff0c;极大的扩展了笔记文档的作用&#xff0c;一个 block 可以是个数据库、多媒体、超链接、公式等等。…

怎么用问卷工具做市场调研?

对于希望开发新产品或服务、拓展新市场或确定潜在客户的公司来说&#xff0c;市场调查是一个至关重要的过程。然而&#xff0c;进行市场调查可能既耗时又昂贵&#xff0c;特别是在涉及对大量人群进行调查的情况下。今天&#xff0c;小编将来聊一聊调查问卷工具如何帮助企业进行…

Rasa 3.x 学习系列-Rasa [3.5.8] -2023-05-12新版本发布

Rasa 3.x 学习系列-Rasa [3.5.8] -2023-05-12新版本发布 当自定义动作设置的值与槽的现有值相同时&#xff0c;将触发SlotSet事件。修复了这个问题&#xff0c;使AugmentedMemoizationPolicy能够正确地处理截断的跟踪器。 为了恢复以前的行为&#xff0c;自定义操作只有在槽值…

【C++进阶】继承详解

文章目录 前言一、继承的概念及定义1.概念2.继承定义定义格式继承关系和访问限定继承基类成员访问方式的变化 二、基类和派生类对象赋值转换三、继承中的作用域四、派生类的默认成员函数五、继承与友元六、继承与静态成员七、复杂的菱形继承及菱形虚拟继承1.单继承与多继承2.菱…

软件 工程

目录 第十章、软件工程1、瀑布模型&#xff08;SDLC&#xff09;2、快速原型模型3、增量模型4、螺旋模型5、Ⅴ模型6、喷泉模型7、构建组装模型&#xff08;CBSD&#xff09;8、统一过程&#xff08;RUP&#xff09;9、敏捷开发方法10、信息系统开发方法11、需求开发12、结构化设…

收藏|必读10本pcb设计书籍推荐

1."High-Speed Digital Design: A Handbook of Black Magic"。 作者是Howard Johnson和Martin Graham。这是一本关于高速数字电路设计的优秀教材&#xff0c;适合那些需要设计高速电路的工程师。 作为比较早出来的信号完整性参考书&#xff0c;对国内的信号完整性研…

H.265/HEVC编码原理及其处理流程的分析

H.265/HEVC编码原理及其处理流程的分析 H.265/HEVC编码的框架图&#xff0c;查了很多资料都没搞明白&#xff0c;各个模块的处理的分析网上有很多&#xff0c;很少有把这个流程串起来的。本文的主要目的是讲清楚H.265/HEVC视频编码的处理流程&#xff0c;不涉及复杂的计算过程。…

第3天学习Docker-Docker部署常见应用(MySQL、Tomcat、Nginx、Redis、Centos)

前提须知&#xff1a; &#xff08;1&#xff09;搜索镜像命令 格式&#xff1a;docker search 镜像名 &#xff08;2&#xff09;设置Docker镜像加速器 详见文章&#xff1a;Docker设置ustc的镜像源&#xff08;镜像加速器&#xff09; 1、部署MySQL 拉取镜像&#xff08;这…

从0到1无比流畅的React入门教程

无比流畅的React入门教程TOC React 是什么 简介 用于构建 Web 和原生交互界面的库React 用组件创建用户界面通俗来讲&#xff1a;是一个将数据渲染为HTML视图的开源JS库 其他信息 Facebook 开发&#xff0c;并且开源 为什么使用React? 原生JS使用DOM-API修改UI代码很繁…

4年外包出来人废了,5次面试全挂....

我的情况 大概介绍一下个人情况&#xff0c;男&#xff0c;毕业于普通二本院校非计算机专业&#xff0c;18年跨专业入行测试&#xff0c;第一份工作在湖南某软件公司&#xff0c;做了接近4年的外包测试工程师&#xff0c;今年年初&#xff0c;感觉自己不能够再这样下去了&…

软件设计模式介绍与入门

目录 1、软件设计模式的起源 2、什么是设计模式&#xff1f; 2.1、设计模式的设计意图 2.2、设计模式的分类准则 3、为什么要学习设计模式 4、如何学习设计模式 5、最后 VC常用功能开发汇总&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&#x…

Redis--弱口令未授权访问漏洞

Redis--弱口令未授权访问漏洞 一、漏洞简介二、危险等级三、漏洞影响四、入侵事件五、漏洞复现--Redis CrackIT入侵事件5.1、以root启动的redis&#xff0c;可以远程登入到redis console--------A主机5.2、生成公钥5.3、执行: redis-cli flushall 清空redis(非常暴力&#xff0…

《终身成长》笔记四——如何面对失败

目录 经典摘录 秉性 一个英雄具备的所有特质 ​编辑 什么是成功 什么是失败 掌控成功 领导力与固定型思维模式 成长型思维模式领导者的行为 害羞 经典摘录 秉性 天才们&#xff0c;因为自己拥有的优势而得意忘形&#xff0c;不去学习如何努力奋斗以及如何面对挫折。…

分享一个程序员接私活、兼职的平台

分享一个程序员接私活、兼职的平台 1、技术方向满足任一即可2、技术要求3、最后 1、技术方向满足任一即可 Python&#xff1a;熟练掌握Python编程语言&#xff0c;能够使用Python进行数据处理、机器学习和深度学习等相关工作。 MATLAB&#xff1a;熟练掌握MATLAB编程语言&…

MathType7公式编辑器新版详细介绍下载安装

由于CSDN这边不能发相关的教程等&#xff0c;若仅用于学习体验&#xff0c;请移步&#xff0c;有能力请支持正版。 wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;mathtype 免费获取MathType-win-zh.exe安装包 它是一款用于数学公式编辑和排版的软件。MathType可以在Mi…

粪菌移植——一种治疗人体疾病的新型疗法

谷禾健康 粪菌移植是一项近年来备受关注的医疗技术&#xff0c;它涉及将健康捐赠者的粪便物质转移至患有疾病或障碍患者的胃肠道。 简单来说就是选择健康合适的人粪便&#xff0c;通过科学方法提取出有用的微生物&#xff0c;去除有害与无用的部分&#xff0c;然后制成制剂&…
最新文章