文章目录
- 7.10 动态链接共享库
- 静态库的缺点
- 何为共享库
- 共享库的"共享"的含义
- 动态链接过程
- 7.11 从应用程序中加载和链接共享库
- 运行时动态加载和连接共享库的接口 dlopen
- 函数 dlsym
- 函数 dlclose
- 函数 dlerror
- 动态加载和链接共享库的应用程序示例
7.10 动态链接共享库
静态库的缺点
- 和所有的软件一样,需要定期维护和更新。如果应用程序员想要使用一个库的最新版本,他们必须以某种方式了解到该库的更新情况,然后显式地将它们的程序与新的库重新链接。
- 几乎每个 C 程序都使用标准 I/O 函数,比如
prinf
和scanf
。在运行时,这些函数的代码会被复制到每个运行进程的文本段中。在一个运行 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.so
和libvector.so
定义的符号的引用。
最后,动态链接器将控制传递给应用程序。从这个时刻开始,共享库的位置就固定了,并且在程序执行的过程中都不会改变。
7.11 从应用程序中加载和链接共享库
到此刻为止,已经讨论了在应用程序执行之前,即应用程序被加载时,动态链接器加载和链接共享库的情景。然而,应用程序还可能在它运行时要求动态链接器加载和链接任意共享库,而无需在编译时链接那些库到应用中。
动态链接是一项强大有用的技术。下面是一些现实的例子:
- 分发软件。微软 Windows 应用的开发者常常利用共享库来分发软件更新。他们生成一个共享库的版本,然后用户可以下载,并用它替代当前的版本。下一次他们运行应用程序时,应用将自动链接和加载新的共享库。
- 构建高性能 Web 服务器。许多Web服务器生成动态内容,比如个性化的 Web 页面、账户余额和广告标语。早期的 Web 服务器通过使用
fork
和execve
创建一个子进程,并在该子进程的上下文中运行 CGI 程序,来生成动态内容。然而,现代高性能的 Web 服务器可以使用基于动态链接的更有效和完善的方法来生成动态内容。
其思路是将生成动态内容的每个函数打包在共享库中。当一个来自 Web 浏览器的请求到达时,服务器动态地加载和链接适当的函数,然后直接调用它,而不是使用 fork
和 execve
在子进程的上下文中运行函数。函数会一直缓存在服务器的地址空间中,所以只要一个简单的函数调用的开销就可以处理随后的请求了。这对一个繁忙的网站来说是有很大影响的。更进一步,可以在运行时,无需停止服务器,更新已存在的函数,以及添加新的函数。
运行时动态加载和连接共享库的接口 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
函数返回一个字符串,它描述的是调用 dlopen
、dlsym
或者 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。