第一章 链接和加载

链接器和加载器有何用处?

 

任何链接器或者加载器所做的工作都很简单:把具体的变量名和抽象的变量名关联起来,使得程序员可以使用更为具体的变量名来编写代码。譬如,它把程序员定义的变量或函数名getline关联到“模块iosys可执行代码开始处612个字节位置处”,或者把一个抽象的数值地址,如“该模块静态变量开始450字节处”关联到一个数值地址。

 

地址绑定:历史发展

一个能够更深刻理解连接器和加载器的用途的有效途径是看一看它们是如何随着计算机编程系统发展而演进的。

最早的计算机完全使用机器语言来进行编程。程序员在纸上写出符号性的程序,手工进行汇编得到机器代码,然后把得到的机器代码装入计算机中,或者通过在纸带或卡片上打孔(开关的触点就能够直接构成代码)。如果程序员使用符号地址,这些符号将在程序员手工进行转换工作时绑定到具体的地址。如果需要添加或者删除某条指令,整个程序都需要仔细检查,任何由于添加或删除指令而受到影响的地址都必须做出相应调整。

 

这里存在的问题名字和地址的绑定进行得太早了。汇编器允许程序员使用符号名字来编写程序,稍后汇编器将符号名绑定到机器地址,从而解决了之前的问题。如果程序有所更改,程序员只需重新进行汇编,但是指定地址的工作已经从程序员身上转移到了计算机。

 

Libraries of code compound the address assignment problem。由于计算机能够执行的基本操作相当简单,所以实用程序一般都由多个实现高层和复杂操作的子程序构成。计算机维护预先编写好经过调试的子程序,程序员可以在他们编写的程序中使用这些子程序,而不是需要他们自己重写子程序。程序员在主程序里加载这些子程序来得到完整的程序。

 

在使用汇编器之前,就已经开始使用子程序库了。1947,领导ENIAC工程的John Mauchly 编写了(待定)

 

随着操作系统的出现,把重定位加载器从连接器和库中分离出来成为必须。在有操作系统之前,程序投入运行时拥有机器的全部内存,因此,只要确定了计算机内存的可用地址后,程序就可以汇编链接到固定内存地指。但是,在有了操作系统后,程序必须和操作系统以及其他程序分享计算机内存。这意味着程序运行时的实际地址只有在操作系统把程序加载到内存后才可知,这就把最后的地址绑定从链接阶段推迟到了加载阶段。现在,连接器和加载器把这个工作分开了,连接器完成地址绑定的部分工作,即在每个程序内指定相对地址,加载器完成最后的重定位工作,指定实际的地址

 

随着系统变得更为复杂,它们需要连接器完成更多更复杂的名字管理和地址绑定工作。Fortran使用了多个子程序和被多个子程序共享的数据----公共快(common block)。为子程序和公共快进行存储的布局和地址指定成为链接器的责任。渐渐地,连接器还必须处理目标代码库,这包括用Fortran和其她语言编写的应用库和编译器支持的被编译的代码隐式调用处理I/O和其他高层操作的库。

 

 很快,程序规模超过了可使用的内存,因此,连接器提供了覆盖机制,一种允许程序员组织程序不同部分共享同一块内存的技巧,当其他部分调用某一被覆盖区域时,按需把这块区域加载进去。1960年左右,随着磁盘的出现,覆盖技术被广泛地应用到大型机上,直到79年代中期虚拟内存推广开来。然后,80年代,以同样的形式在微型机上重新出现。随着90年代虚拟内存在PC机上出现,再一次退去。但是,在内存有限的嵌入式环境仍在使用,其他需要控制内存使用以提高性能的情况下可能会重新出现。

 

 随着硬件重定位和虚拟内存的出现,连接器和加载器实际上变得不那么复杂,因为每个程序又重新拥有了整个地址空间。程序可以被连接加载到固定地址处,由硬件而不是软件重定位管理加载时的重定位但是,具有硬件重定位的计算机必定会运行不止一个程序,经常是一个程序的多个副本。当计算机运行一个程序的多个实例时,又一部分对于所有运行中的实力都是一样的(具体来说,可执行代码),其他部分则是唯一的。如果不变的部分可以从变化部分分离出来,对于运行时不会变化的部分,操作系统则可以使用单个拷贝来节省大量的存储空间。编译器和汇编器经过修改,可以生成具有多个段(section)的目标代码,其中,一个段放只读代码,另外的段放数据。连接器则要能够把每种类型的段结合在一起,使得链接得到的程序中,所有代码放到一个地方,所有的数据放到另外一个地方。不像以前那样,这里并未有推迟地址绑定,因为地址仍是在链接时指定的,但是,更多的工作被推迟到连接器以为所有的段指定地址。

 

。。。。。。。。(待定)

 

链接器VS加载器

连接器和加载器完成一系列相关但不同概念的工作。

 

程序加载:从二级存储器(磁盘)拷贝程序到主存储器准备运行。某些情况下,加载仅仅是把数据从磁盘拷贝到内存。其他情况下,也可能包括分配存储空间,设定保护位,或者将虚拟地址映射到磁盘页。

重定位:通常编译器和汇编器生成的目标代码中,程序地址从零开始,但是极少有计算机允许你将程序加载到地址零处。如果一个程序是由多个子程序构成的,所有的子程序必须加载到没有相互覆盖的地址处。重定位就是一个为程序的不同部分指定加载地址,调整程序中代码和数据以反应指定地址的过程。对于链接器来说,由多个子程序构成一个程序是很常见的,这个链接生成的程序从零地址开始,各个子程序在这个链接生成的大程序中重新定位地址。当这个程序被加载时,系统选择一个实际加载地址,该程序作为一个整体被重定位到加载地址处。

符号解析:当从多个子程序构建程序时,通过使用符号完成一个子程序对另一个子程序的引用。一个主的程序可能使用到了名为sqrt的求平方根程序,数学库则定义了sqrt。通过标示出sqrt在库中指定的位置,同时在调用者的目标代码中

插入调用指令引用到的那个位置信息。

 

尽管链接和加载之间存在大量的重叠 ,但将完成加载工作的程序定义为加载器和完成符号解析的程序定义为链接器比较合理。两者都可以完成重定位,且有将这三个功能集成到一个链接加载器中去的。

 

重定位和符号解析间的接线可能模糊。既然链接器已经能够解析引用到的符号,处理代码重定位的途径之一是

 为程序的每个部分的基地址指定一个符号,然后将可重地位地址视为对基地址的引用。

 

 一个链接器和加载器共有的特性是两者都要在目标代码中插入相关信息,the only widely used programs to do so other than peraps debuggers。这是一个独特且功能强大的特性,虽然细节上和特定的机器机器相关,且出现错误的话将导致莫名其妙的bug。

 

两阶段链接

现在我们转向链接器的大体结构上来。链接,像编译和汇编一样,大体上也分为两个阶段。链接器接收一组目标文件、库,还可能是命令文件作为其输入,生成一个目标文件,也可能还有一些辅助信息,诸如加载映像或者包含用于调试的符号的文件。如图1:

 

 每个输入文件包含了多个段(segment),也就是将被放置到输出文件中的连续的代码或数据块。每个输入文件也包含至少一个符号表(symbol table)。有些符号是导出的(exported),即在一个文件中定义而在别的文件中使用的,通常是那些在一个文件中定义可以从别处调用的函数(routine)名。有些符号是导入的(imported),即在一个文件中使用但未被定义的,通常是那些被调用但不在本文件中定义的函数名。

 

当链接器开始运行,它首先扫描输入文件确定各个段(segment)的大小,收集所有符号的定义(definition)和引用。它生成一个段表(segment table)列出所有在输入文件中定义的段,生成一个包含了所有导入和导出符号的符号表(symbol table)。

 

使用第一个阶段得到的数据,链接器可以为符号指定数值位置(numeric location),确定输出地址空间中段的大小和位置,解决输出文件中各个部分放到何处。

 

 

第二阶段使用在第一阶段收集到的信息来控制实际的链接过程。它读取并重定位目标代码,用数值地址代替符号引用,调整代码和数据的内存地址以反映重定位过的段(segment)地址,并将重地位后的代码写到输出文件。输出文件通常包含文件头信息,被重定位的段,和符号表信息。如果程序使用到了动态链接,符号表包含运行时连接器(runtime linker)解析动态符号所需的信息。很多情况下,连接器本身要产生少量的代码或数据,如用来调用覆盖区或动态链接库,或者是指向程序启动时调用的初始化例程的指针数组的“glue code”、

 

不管程序是否使用动态链接库,生成的文件都可能包含一个用于重连接(relinking)和调试的符号表,程序本身并不使用但别的处理输出文件的程序可能使用的符号表

 

有些目标格式是可重连接的(relinkable),也就是,链接器运行时生成的输出文件可以用作稍后连接器运行时的输入文件。这要求输出文件包含一个类似输入文件中一样的符号表,以及其他所有输入文件中的辅助信息

 

、、、、、待定

 

重定位和代码修改

连接器和加载器的主要动作是重定位和代码修改。当一个编译器或汇编器生成目标文件时,它生成使用未经过重定位的在文件内定义的代码和数据地址,对于在别处定义的代码和数据这些地址通常设置为零。作为链接过程的一部分。连接器修改目标代码以反映实际指定的地址。例如,看一段使用eax寄存器传递变量a到b的x86代码片段。

 

mov a, %eax

mov %eax ,b

 

如果a是在同一文件的0x1234处定义,而b是从别处导入的,生成的目标代码将会是:

A1 34 12 00 00  mov a, %eax

A3 00 00 00 00  mov %eax, b

每条指令包含一个字节的操作码,后面跟了四个字节的地址。第一条指令引用了1234(字节反转了),因为位置b未知,所以第二个引用设置为了零

 

现在假设连接器对这段代码进行了连接之后,a被重定位到10000B处,b引用的是9a12,那么连接器将代码修改为:

 

 

A1 34 12 01 00  mov a, %eax

A3 12 9a 00 00  mov %eax, b

 

即,链接器在第一条指令(中包含的)地址上加10000(hex),所以现在引用的a重地位后的地址为11234,同时,它还修改了b的地址。这些调整对指令有影响,但是目标文件中数据部分的指针也必须进行调整。

 

待续。。。。。。。。

 

编译器驱动器(compiler Drivers)
多数情况下,链接器所进行的操作对程序员是不可见的,或者差不多是这样的。因为它作为编译过程的一部分自动运行了。大多数的编译系统有一个称为编译驱动器(compiler Drivers)的东西,在编译过程中自动调用所需的相关程序。例如,如果程序源有两个C源文件,在Unix系统上,编译驱动器将依次运行如下程序:
1.为文件A调用C预处理器,生成预处理A
2.为与处理过的A运行C编译器,生成A的汇编文件
3.运行汇编器,生成A的目标文件
4.为文件A调用C预处理器,生成预处理A
5.为与处理过的A运行C编译器,生成A的汇编文件
6.运行汇编器,生成A的目标文件
7.运行链接器,链接A、B的目标文件以及系统C库

 

待续。。。。。。。。

 

链接:一个真实的例子

 

//source file m.c

extern void a(char *);

int main(int ac ,char **av)

{

    static char string[] = "Hello, world!/n";

    a(string);

}

 


//source file a.c
#include <unistd.h>
#include <string.h>

void a(char *s)
{
    write(1,s,strlen(s));
}

 

 
主程序m.c 用GCC在我的Pentium上编译成165字节的a.out格式的目标文件。这个目标文件包括一个固定长度的文件头,16字节包含了只读程序代码的“text段 ,16字节包含了字符串的“data”段。接下来是两个重定位项(entry),一个标示了pushl指令,它将字符串的地址入栈为调用a做准备。另一个标示了调用指令,它将控制权移交给a。符号表将定义 _main导出,导入_a,还包含一些其他与调试有关的符号(每一个全局变量都加上了一个前缀下划线)。注意pushl指令引用到了位置0x10,这是字符串暂时的地址,因为处在同一个目标文件中,而调用指令引用的地址为0,因为地址  _a还未知。


//object code for m.o
Sections:
Idx     Name             Size        VMA        LMA         File off Algn
0       .text            00000010    00000000   00000000    2**3
1       .data            00000010    00000000   00000000    2**3

Disassembly of section .text:

0000000 <_main>:
    0:   55                    pushl   %ebp
    1:   89 e5                mvol    %esp,%ebp
    3:   68 10 00 00 00   pushl   $0x10
       4:   32   .data
    8:   e8 f3 ff ff ff        call     0
       9:DISP32 _a
   d:    c9                     leave
   e:    c3                     ret

 

待续。。。。。。。。

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值