1、可执行目标文件
注:ELF(Executable and Linkable Format)
从上图中可以看出,代码段的地址总是比数据段的地址小。
2、加载可执行目标文件
任何Unix程序都可以通过调用execve函数来调用加载器。加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第1条指令,即入口点(Entry Point),来运行该程序。将程序拷贝到存储器并运行的过程叫做加载(loading)。
每个Unix程序都有一个运行时存储器映像,如图中。在Linux系统中,代码段总是从地址0x08048000处开始。数据段是在接下来的下一个4KB对齐的地址处。堆在接下来的读/写段之后的第一个4KB对齐的地址处,并通过调用malloc库往上增长。从0x4000000处的段是为共享库保留的。用户栈总是从地址0xbfffffff处开始,并向下增长的(高地址向低地址增长的)。从栈的上部开始于地址0xc0000000处的段是为操作系统驻留存储器的部分(也就是内核)的代码和数据保留的。
3、动态链接共享库
共享库是一个目标模块,在运行时,可以加载到任意的地存储器地址,并在存储器中和一个程序链接起来。这个过程称为动态链接(dynamic linking),是由一个叫做动态链接器(dynamic linker)的程序执行的。
共享库也称为共享目标(shared object),在Unix系统中通常用.so后缀来表示。(在MS OS 中为DLL文件)
注意:静态链接与动态链接的区别:静态链接是把程序所需要的库代码和数据拷贝和嵌入到引用它们的可执行文件中;而动态链接是所有引用该库的可执行文件文件共享这个.so(dll)文件中的代码和数据。
4、从应用程序中加载和链接共享库
通过几个函数,dlopen加载和链接共享库,dlsym通过输入的共享库的符号名字,返回符号的地址;dlclose卸载共享库,dlerror返回前面函数执行情况的一个字符串。
示例代码
#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() 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; }
5、PIC(与位置无关的代码,Position-independent code)
共享库的一个主要目的就是允许多个正在运行的进程共享存储器中相同的库代码,因而节约存储器资源。
PIC:编译库代码,不需要链接器修改库代码,就可以在任何地址加载和执行这些代码。
在一个IA32系统中,对同一个目标模块中过程的调用不需要特殊处理的,因为引用是PC相关的,已知偏移量,就是PIC了。然而,对外部定义的过程调用和对全局变量的引用通常不是PIC,因为它们都要求在链接时重定位。
如何对全局变量生成PIC引用呢?基于以下事实:无论我们在存储器中的何处加载一个目标模块(包括共享模块),数据段总是分配为紧随在代码段后面。因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量。
关于具体示例,可以参见本书7.12节。
6、处理目标文件的工具
GNU binutils包。如objdump,ar,ldd。
7、链接可以在编译时由静态编译器完成,也可以加载和运行时由动态编译器完成。链接器处理称为目标文件的二进制文件,它有三种不同的形式:可重定位的,可执行的,和共享的。可重定位的目标文件由静态链接器组合成一个可执行的目标文件,它可以加载到存储器中并执行。共享目标文件(共享库)是在运行时由动态链接器链接和加载的,或者隐含地在调用程序被加载和开始执行时,或者根据需要在程序中调用dlopen库的函数时。
The main tasks of linkers are symbol resolution, where each global symbol is
bound to a unique definition, and relocation, where the ultimate(最终的)memory
address for each symbol is determined and where references to those objects are
modified.
<Computer Systems:A Programmer's Perspective>