加载可执行目标文件

下图概括了一个典型的ELF可执行文件中的各类信息。


通过调用某个驻留在存储器中称为加载器(loader)的操作系统代码来运行可执行文件。任何 UNIX 程序都可以通过调用 execve 函数来调用加载器。

加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第一条指令或入口段(entry point)来运行该程序。这个将程序拷贝到存储器并运行的过程叫做加载(loading)。

当加载器运行时,它创建一个存储器映像。在可执行文件中段头部表的指导下,加载器将可执行文件的相关内容拷贝到代码和数据段。接下来,加载器跳转到程序的入口点,也就是符号 _start 的地址。在 _start 地址处的启动代码(startup code)是在目标文件 ctl1.o中定义的,对所有的 C 程序都是一样的。

动态链接共享库

静态库有一些明显的缺点。

静态库和所有的软件一样,需要定期维护和更新。如果应用程序员想要使用一个库的最新版本,他们必须以某种方式了解到该库的更新情况,然后显式地将他们的程序与更新了的库重新链接。

另一个问题是几乎每个 C 程序都使用标准的 I/O 函数,如 printf 和 scanf。在运行时,这些函数的代码会被复制到每个运行进程的文本段中。在一个运行 50~100 个进程的典型系统上,这将是对稀缺的存储器系统资源的极大浪费。

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

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

为了构造上图中向量运算示例程序的共享库libvector.so,我们会调用编译器,给编译器如下特殊指令:

gcc -shared -fPIC -o libvector.so addvec.c multvec.c

-fPIC选项指示编译器生成与位置无关的代码。编译库代码,使得不需要链接器修改库代码就可以在任何地址加载和执行这些代码。这样的代码叫做与位置无关的代码(Position-Independent Code, PIC)。用户对 GCC 使用-fPIC 选项指示 GNU 编译系统生成 PIC 代码。

-shared选项指示链接器创建一个共享的目标文件。

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

这样就创建了一个可执行文件 p2,而此文件的形式使得它在运行时可以和 libvector.so链接。基本的思路是当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程。

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

当加载器加载和运行可执行文件 p2 时,加载部分链接的可执行文件 p2。接着,它注意到p2 包含一个 .interp 节,这个节包含动态链接器的路径名,动态链接器本身就是一个共享目标(比如,在 Linux 系统上的 ld-linux.so)。加载器不再像它通常那样将控制传递给应用,而是加载和运行这个动态链接器。

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

  • 重定位 libc.so 的文本和数据到某个存储器段
  • 重定位 libvector.so 的文本和数据到另一个存储器段
  • 重定位 p2 中所有对 libc.so 和 libvector.so 定义的符号的引用

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

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

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

动态链接在现实世界中的例子:分发软件,构建高性能web服务器。

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

1
2
3
4
5
6
7
8
9
10
11
12
#include <dlfcn.h>
void *dlopen(const char * filename, int floag);
//成功,就返回指向句柄的指针,这里的句柄应该是指一个目标文件吧。失败,就返回NULL

void * dlsym(void *handle, char *symbol);
//第一个参数是上面函数返回的句柄的指针,第二个参数是符号的名字,如果符号存在就返回符号的地址,这里符号的地址应该是目标文件的符号表中的符号的地址吧,否则返回NULL

int dlclose(void *handle);
//如果没有其他共享库正在使用这个共享库,那么就卸载该共享库。这里值得考虑,没有其他共享库,而不是没有其他函数。卸载共享库。

const char* dlerror(void);
//上面的3个函数运行之后,运行这个函数,可以看看最近发生的最近的错误,如果没有错误,就返回NULL

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/** dll.c **/
#include <stdio.h>
#include <stdlib.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() it 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:
gcc -rdynamic -O2 -o p3 dll.c -ldl

-rdynamic 用来通知链接器将所有符号添加到动态符号表中(目的是能够通过使用 dlopen 来实现向后跟踪)

-ldl 指示链接器链接一个库,这个库里包含了 dlopen, dlsym 等等的函数,也就是说,是支持“在运行时,显示加载使用动态连接库”的函数库。相关的头文件是 dlfcn.h。

处理目标文件的工具

小结


参考资料:

  1. 《深入理解计算机系统》