链接可以执行与编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到存储器并执行时;甚至可以执行于运行时,由应用程序来执行。
从传统静态链接到加载时的共享库的动态链接,以及到运行时的共享库的动态链接。
/* $begin main */ /* main.c */ void swap(); int buf[2] = {1, 2}; int main() { swap(); return 0; } /* $end main */
/* $begin swap */ /* swap.c */ extern int buf[]; int *bufp0 = &buf[0]; int *bufp1; void swap() { int temp; bufp1 = &buf[1]; temp = *bufp0; *bufp0 = *bufp1; *bufp1 = temp; } /* $end swap */
函数main()调用swap交换外部全局数据buf中的两个元素。这个例子贯穿全文,分析链接是如何工作的。但是上述的交换方式非常奇怪!!!大多数编译系统提供编译驱动程序,它代表用户在需要时调用语言预处理器、编译器、汇编器和链接器。
静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出。输入的可重定位目标文件由各种不同的代码和数据节组成。指令在一个节中,初始化的全局变量在另一个节中,而未初始化的变量又在另外一个节中。
为构造可执行文件,链接器必须完成两个主要任务:
目标文件有三种形式:
编译器和汇编器生成可重定义目标文件(包括共享目标文件)。链接器生成可执行目标文件。
各个系统之间,目标文件格式都不相同。
一个典型的ELF可重定位目标文件包含下面几个节:
在32位linux系统中,代码段总是从地址0x08048000处开始。数据段是在接下来的一个4KB对齐的地址处。运行时堆在读/写段之后接下来的第一个4KB对齐的地质处,并通过调用malloc库往上增长。用户栈总是从最大的合法用户地址开始,向下增长的。
加载的工作流程:
UNIX系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当外壳运行一个程序时,父外壳进程生成一个子进程,它是父进程的一个复制品。子进程通过execve系统调用启动加载器。加载器删除子进程现有的虚拟存储器段,并创建一组新的代码、数据、堆和栈段、新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到存储器的数据拷贝。直到CPU应用一个被映射的虚拟页才会进行拷贝,此时,操作系统利用它的页面调度机制自动将页面从磁盘传送到存储器。
ps:链接可以在编译时由静态编译器来完成,也可以在加载时和运行时由动态链接器来完成。链接器处理称为目标文件的二进制文件。