深入理解计算机系统 第七章
连接器完成的两个主要任务:符号解析和重定位
编译器和汇编器生成地址0开始的代码和数据节
目标文件:可重定位目标文件,可执行目标文件,共享目标文件
ELF 可重定位目标文件:ELF头以一个16字节的序列开始,这个序列描述了字的大小和生成该文件的系统的字节顺序. .test.:已编译程序的机器代码;.rodata.:只读数据;.data.:已初始化的全局变量;.bss.:为初始化的全局变量;.symtab.:存放程序中被定义和引用的函数和全局变量的信息
连接器上下文中有三种符号:块内定义的全局符号;块外定义的全局符号和本地符号;
定义为带c static属性的本地过程变量不是在栈中管理的,编译器在.data.和.bss.中为每个定义分配空间;
任何声明为static属性的全局变量或函数是模块私有的;
编译器来保证本地符号的唯一性;
当编译器遇到一个不是在当前模块中定义的变量时,它会假设该符号是在其他某个模块中定义的,生成一个连接器符号表表目
运行时的存储器映像:未使用->只读段->读写段->运行时堆->共享库的存储器映射区域->用户栈->内核虚拟存储器
加载运行:可执行文件中段头表的指导下,加载代码和数据->程序入口点_start->初始化函数->注册函数->主函数->返回
c的启动代码对于每个c程序都是相同的,都需要跳到main函数
链接就是将不同部分的代码和数据收集和组合成为一个单一文件的过程,这个文件可被加载到存储器并执行.链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到存储器并执行时;甚至执行于运行时,由应用程序来执行
链接器在软件开发中扮演着一个关键的角色,因为他们使得分离编译成为了可能;
目标文件纯粹是字节块的集合,这些块中,有些包含程序代码,有些则包含程序数据,而其他的则包含指导链接器和加载器的数据结构.链接器将这些块连接起来,确定链接块的运行时位置,并且修改代码和数据块中的各种位置.链接器对目标机器了解甚少,产生目标文件的编译器和汇编器已经完成了大部分工作
目标文件有三种形式:
1.可重定位目标文件包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件
2.可执行目标文件包含二进制代码和数据,其形式可以被直接拷贝到存储器并执行
3.共享目标文件一种特殊类型的可重定位目标文件,可以在加载活着运行时,被动态的加载到存储器并链接
各个系统之间,目标文件的格式都不相同;
SUN Solaris使用的是Unix ELF(可执行可链接格式)
ELF 头以一个16字节的序列开始,这个序列描述了字的大小和生成该文件的系统的字节顺序.ELF头剩下的部分包含帮助链接器解析和解释目标文件的信息.其中包括了ELF头的大小,目标文件的类型(可重定位,可执行或是共享等),机器类型(IA32等),节头部表的文件偏移,以及节头部表中的表目大小和数量.不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的表目
每个可重定位目标文件m都有一个符号表,它包含m所定义和引用的符号的信息.在链接器的上下文中,有三种不同的符号:
1.由m定义并能被其他模块引用的全局符号.全局链接器符号对应于非静态的c函数以及被定义为不带c的static属性的全局变量
2.由其他模块定义的并被模块m引用的全局符号.这些符号称为外部符号,对应于定义在其他模块中的c函数和变量
3. 只被模块m定义和引用的本地符号.有的本地链接器符号对应于代static属性的c函数和全局变量.这些符号在模块m中的任何地方都是可见的,但是不能被其他模块引用.
.symtab中的符号表不包含对应于本地非静态程序变量的任何符号.这些符号在运行时在栈中被管理,链接器第此类符号不感兴趣
c程序员使用static属性在模块内部隐藏变量和函数声明
链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来
编译器只允许每个模块中的每个本地符号只有一个定义,编译器还确保静态本地变量,他们也也会有本地链接器符号,拥有唯一的名字
当编译器遇到一个不是在当前模块中定义的符号(变量或是函数)时,它会假定该符号是在其他模块中定义的,生成一个链接器符号表表目,并把它交给链接器处理
c++和java中能使用重载函数,是因为编译器将每个唯一的方法和参数列表组合编码成一个对链接器来说是一个唯一的名字.这种编码过程叫做毁坏,而相反的过程叫做恢复
函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号
根据强弱符号的定义,Unix链接器使用下面的规则来处理多处定义的符号:1.不允许有多个强符号;2.如果有一个强符号和多个弱符号,那么选择强符号;3.如果有多个弱符号,那么从这些弱符号中任意选择一个
所有的编译系统都提供一种机制,将所有相关的目标文件打包为一个单独的文件,称为静态库,它也可以作为链接器的输入
在Unix系统中,静态库以一种称为存档的特殊文件格式存放在磁盘中.存档文件是一组连接起来的可重定位目标文件的集合,有一个头部描述每个成员目标的大小和位置
在符号解析的阶段,Unix链接器从左到右按照它们在编译器驱动程序命令行上出现的相同顺序来扫描可重定位目标文件和存档文件
一旦链接器完成了符号解析这一步,它就把代码中的每个符号引用和确定的一个符号定义(也就是它的一个输入目标模块中的一个符号表表目)联系起来.在此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小.现在就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址. 重定位由两部分组成:重定位节和符号定义(这步完成时,程序中的每个指令和全局变量都有一个唯一的运行时存储器地址了);重定位节中的符号引用
当汇编器生成了一个目标模块时,它并不知道数据和代码最终将存放在存储器的什么位置.它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置.所以,无论何何时编译器遇到对最终位置未知的目标引用,它就会生成一个重定位表目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用,代码的重定位表目放在.relo.text中,已初始化数据的重定位表目放在.relo.data中
可执行文件的.init节定义了一个小函数,叫做_init,程序的初始化代码会调用它
ELF可执行文件被设计为一个很容易加载到存储器,连续的可执行文件组块被映射到连续的存储器段.段头表描述了这种映射关系
加载器将可执行目标文件中的代码和数据从磁盘拷到存储器中,然后通过跳转到程序的第一条指令,即入口点,来运行该程序.这个将程序拷贝到存储器并运行的过程叫做加载
当加载器运行时,它创建了一个存储器映像.在可执行文件中段头表的知道下,加载器将可执行文件的相关内容拷贝到代码和数据段,接下来,加载器跳转到程序的入口点,也就是符号_start的地址._start地址处的启动代码是在目标文件ctrl.o中定义的,对所有的c程序都是一样的.在从.text 和.init节中调用了初始化例程后,启动代码调用atexit例程,这个程序附加了一系列在应用调用exit函数时应该调用的程序.exit函数运行 atexit注册函数,然后通过调用_exit将控制返回给操作系统,接着,启动代码调用应用程序的main程序,这就开始执行我们的c代码了,在应用程序返回之后,启动代码调用_exit程序,它将控制返回给操作系统
Unix系统中的每个程序都运行在一个进程上下文中,这个进程上下文有自己的虚拟地址空间,当shell运行一个程序时,父shell进程生成一个子进程,他是父进程的一个复制品.子进程通过execve系统调用启动加载器.加载器删除子进程已有的虚拟存储器段,并创建一组新的代码,数据,堆和栈段.新的栈和堆段被初始化为零.通过将虚拟地址空间中的页映射到可执行文件的页大小的组块,新的代码和数据段被初始化为可执行文件的内容.最后,加载器跳转到_start地址,它最终会调用应用的main函数.除了一些头部信息,在加载过程中没有任何从磁盘到存储器的数据拷贝.直到CPU引用一个被映射的虚拟页,才会进行拷贝,此时,操作系统利用它的页面调度机制自动将页面从磁盘传送到存储器
共享库是致力于解决静态库缺陷的一个现代创新产物.共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并在存储器中和一个程序链接起来.这个过程称为动态链接,是由一个叫做动态链接器的程序来执行的
在存储器中,一个共享库的.text节只有一个副本可以被不同的正在运行的进程共享
生成动态链接的可执行文件时,链接器拷贝了一些重定位和符号表信息,它们使得运行时可以解析对动态库中代码和数据的引用
加载器将可执行文件的内容映射到存储器,并运行这个程序.链接器还可能生成部分链接的可执行程序,这样的文件中有未解析的到定义在共享库中的程序和数据的引用.在加载时,加载器将部分链接的可执行文件映射到存储器,然后调用动态链接器,它通过加载共享库和重定位程序中的引用来完成链接