linkers和loaders小述

来源:互联网 发布:期货模拟软件 编辑:程序博客网 时间:2024/05/09 03:50

  最早的计算机程序是由机器语言编写的。程序员也可先编写符号形式的汇编程序,然后手工汇编为机器码,再交付给计算机执行。程序员在手工汇编时需要自己确定符号地址;这样做的弊端是,一旦程序稍有改动,相关的符号地址都必须进行修正。

       产生这种弊端的原因是过早地将符号(变量和地址标号)与其地址绑定在一起。于是出现了assembler,当程序完成后,由assembler来完成符号的地址翻译工作。

       代码库的出现使得地址分派进一步复杂化。库文件是一些有用的小程序的集合。程序员可以调用其中的子程序来构成一个完整的程序。实际上,库的历史比assembler更久远。库中的子程序都假定是从地址0开始的,当它被链接到一个特定的主函数中时,由重定位加载器(relocating loader)来确定其实际的地址。

       随着操作系统的出现,relocating loader从链接器(linker)中分离出来。这是因为,没有操作系统时,一个程序可以占有机器的整个存储空间,因而程序可以被汇编和链接到固定的内存地址;而操作系统出现后,程序则要与操作系统和其他程序共享计算机的内存,某个程序执行时的实际地址在操作系统把它加载到内存之前是无法预知的,这样就把最终的地址绑定从链接时推后到加载时。于是linkersloaders进行了分工,linkers作部分地址绑定,指派各个程序内部的相对地址;loaders则在最后的重定位阶段指派实际地址。

       程序变得越来越庞大,甚至超过了系统的内存容量,因此linkers又提供了覆盖(overlay)的功能,使得程序员可以为同一个程序的不同部分安排共享空间,根据需要在调用时进行覆盖加载。Overlay1960年磁盘出现直到1970年虚拟内存技术的发展,广泛应用于大型主机上;20世纪80年代早期又重现于微型计算机,到90年代虚拟内存技术应用于PC机后又消逝。这一技术仍然被用于存储空间受限的嵌入式领域。

       随着硬件重定位和虚拟内存的出现,linkersloaders实际上没有这么复杂了,因为每个程序又可以占有整个地址空间了。程序可以链接到要加载的固定地址,并使用硬件方法而非软件重定位来进行加载时的重定位。但计算机通常会运行某个程序的多个实例,每个实例中某些部分是与其他实例相同的(只读的代码部分),某些部分则是各不相同的(可读写的数据部分),于是compilerassembler又改进为将程序分段(section)处理,例如分为代码段和数据段,这样在运行多个实例时,就可以只要一分代码段的拷贝,以节省内存空间。地址仍在链接时指派,但更多工作则推迟到linker为所有的sections指派地址。

       即使运行在同一计算机上的不同程序,也会共享部分代码,这就是库,分为静态共享库和动态共享库。静态共享库在建库时就被绑定到特定地址,linker则在链接时把程序中对库函数的引用也绑定到特定地址;静态库的方便性和灵活性欠缺,因为一旦库发生改变,所有的程序必须重新链接,建立静态共享库的过程也很繁琐。而动态链接库则不同,库中的段和符号直到使用该库的程序运行时(或实际调用该库时)才会被绑定到实际地址。

 

Linking vs. loading

linkersloaders执行几个相关的但是概念上分离的操作:

(1)       Program loading:将程序从二级存储器(辅存如disk)拷贝到主存以准备运行。某些情况下还包括分配存储单元,设置保护位,或安排虚拟内存映射到磁盘页面的虚拟地址空间。

(2)       RelocationCompilersassemblers通常建立起始地址为0的目标文件,但极少数计算机允许你将程序加载到地址0Relocation就是为程序的各个部分指派加载地址,调整程序中的代码和数据到指派地址的过程。多数系统上,relocation通常不止一次。linker会把多个子程序链接为一个程序,并建立一个链接好的起始地址为0的输出程序,这个过程实际上包括了把各个子程序重定位(relocate)到不同地址的过程。当程序加载时,系统则选择实际的加载地址,并把这个链接好的程序再次重定位(relocate)到加载地址。

(3)       Symbol resolution:多个子程序之间的引用是通过符号完成的。linker使用符号的地址来对其进行引用。

尽管链接和加载之间有很多相交之处,我们仍然有理由把进行程序加载的定义为loader,把进行符号解析的定义为linker。它们都能进行重定位,甚至还存在同时具备上述三种功能的linking loaders

  

在现代体系结构上,产生PIC可执行代码并非难事。JumpsBranches只要是PC-relative或是基于运行时所设置的基址寄存器的即可,这样就不需要在加载时对程序进行重定位。为题在于数据寻址。代码不能直接使用数据地址寻址,否则需要将数据重定位到特定地址,这样就不是PIC了;通常的解决办法是建立一个数据地址表,并使用一个寄存器指向该表,这样代码就可以使用该基址寄存器对数据进行索引——即使如此,还有一个问题是该基址寄存器如何获取第一个数据地址。

对于ELF格式的可执行文件,通常是代码段后面跟着数据段,并且两者之间的偏移量(offset)是固定的,因此只要代码段中的代码将其自身的地址加载到一个寄存器中,数据段的地址与该地址就是一个确知的距离。linker会为可执行文件所寻址的全局数据建立一个GOTglobal offset table),该表中包含了所有的数据指针。动态链接器(ld.so)则对GOT中的指针进行解析和重定位。

原创粉丝点击