程序的链接过程

编译系统

程序的生命周期是从一个高级语言程序开始,为了运行这个程序,必须首先将其转换为一系列低级机器语言指令。然后这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来。目标程序也称为可执行目标文件

在Unix系统上,从源文件到目标文件的转化是由编译器驱动程序来完成的。gcc编译器驱动程序读取源程序文件,并把它翻译成一个可执行目标文件。这个翻译过程可以分为四个阶段:预处理阶段,编译阶段,汇编阶段,链接阶段。这四个阶段的程序(预处理器,编译器,汇编器,链接器)共同组成编译系统。

1
2
3
4
5
6
//hello.c
#include<stdio.h>
int main(){
printf("hello world\n");
return 0;
}

  • 预处理阶段:预处理器根据以字符#开头的命令,修改原始C程序,替换为其定义的内容,这里#include将告诉预处理器读取系统头文件stdio.h的内容,插入到程序文本中,结果得到另一个C程序,通常以.i作为文件扩展名。
  • 编译阶段:编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇报语言程序。汇编语言为不同的高级语言的不同编译器提供了通用的输出语言。
  • 汇编阶段:汇编器将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中,也就是说,hello.o文件是一个二进制文件
  • 链接阶段:链接器负责处理静态链接和动态链接,结果得到一个可执行目标文件,可以被加载到内存中,由系统执行。

静态链接

为了构造可执行文件,链接器必须完成两个主要任务:

  • 符号解析:目标文件定义和引用符号,每个符号对应于一个函数,一个全局变量或一个静态变量。符号解析的目的就是将每个符号引用正好和一个符号定义关联起来。对于多重定义的全局符号,可划分为:强符号和弱符号。链接器的解析规则为:①不允许有多个同名的强符号。②如果一个强符号和多个弱符号同名,那么选择强符号。③如果多个弱符号同名,则从这些弱符号中随机选择一个。
  • 重定位:编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。

有必要注意下关于链接器的事实:目标文件纯粹是字节块的集合。这些块中,有些包含程序代码,有些包含程序数据,其他的包含引导链接器和加载器的数据结构。链接器将这些块链接起来,确定被链接块的运行时位置,并且修改代码和数据块中的各种位置。

目标文件

目标文件有三种形式:可重定位目标文件,可执行目标文件,共享目标文件。编译器和汇编器生成可重定位目标文件(包括共享目标文件),链接器生成可执行目标文件。从技术上说,,一个目标模块就是一个字节序列,一个目标文件就是一个以文件形式存放在磁盘中的目标模块。

将可执行目标文件从磁盘加载到内存中运行需要加载器来实现,加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序。将程序从磁盘复制到内存并运行的过程叫做加载。程序运行时都会被分配一个相同的内存映像,从虚拟地址0x00000000到0xffffffff,从低地址到高地址依次划分为:代码段(0x40000000开始),已初始化数据段,未初始化数据段,堆,共享库映射区域,栈,内核段。每次程序运行时,这些区域的(物理)地址都会改变,但是它们的相对位置不会变。

顺便说下加载器的工作过程:当shell运行一个程序时,父shell进程生成一个子进程,它是父进程的一个复制。子进程通过execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码,数据,堆和栈段。新的栈和堆被初始化为0.通过将虚拟地址中的页映射到可执行文件的页大小的片。新的代码和数据被初始化为可执行文件的内容。最后加载器跳转到_start地址,它最终会调用应用程序的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制。此时操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。


参考资料:

《深入理解计算机系统 3th》(CSAPP)