编译器系统之链接

对链接的相关内容,作了如下总结:

 

编译器驱动程序

 

首先讲到链接,即会涉及到一个概念,编译器驱动程序,编译器驱动程序即编译器系统为用户,根据需求调用相应语言的预处理器,编译器,汇编器,以及链接器。我们举个例子:

--------main.c----------

#include <stdio.h>

void func1();

int main()

{

        printf(“welcome to CSDN,call the func1!”);

        func1();

        return 0;

}

--------func1.c----------

#include <stdio.h>

void func1()

{

        printf(“now calling the func1 function”);

}

linux下,保存为main.c文件和func1.c文件,执行如下命令:

gcc –O2 -g –v –o out main.c func1.c

gcc编译系统将调用预处理器,编译器,汇编器以及连接器生成out文件,该文件为一个可执行目标文件。

看到这里,该讲讲我们的链接过程,如图所示:(图片暂未能上传,后续补上,下同)

       其中,翻译器指的是预处理器,编译器以及汇编器处理的三个过程,生成可重定位的目标文件main.o func1.o,而接下来要讲的链接器就是将可重定位目标文件链接成可执行的目标文件out

链接的类型

链接的类型:

1.静态链接;

2.加载器加载目标文件时动态链接;

3.目标文件运行时动态链接;

需要说明的是,这三种链接类型的本质大概一直,因此,这里主要讲的是静态链接,关于加载时的动态链接以及运行时的动态链接只是稍微提及;              

静态链接

Unix的静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的(即已经完成重定位的)的可以加载和运行的可执行目标文件作为输出;

 

        为了创建可执行文件,连接器主要完成两项任务:

l       符号解析:即将每个目标文件的每个符号引用和相应的符号定义关联起来;

l       重定位:我们知道,编译器和汇编器生成从地址零开始的代码和数据节(关于节的概念后边会提及),重定位的目的是将目标文件的符号定义与存储器位置关联起来,然后修改每一目标文件对符号定义的引用指向这个存储器的位置,从而重定位代码和数据节。

上面提到的节,即可重定位目标文件的格式是由各种不同的代码和数据节组成。

目标文件

上边我们一直提到的目标文件,有一下三种形式:

 

l       可重定位目标文件:包含二进制代码和数据,在编译时可与其他可重定位目标文件合并起来,生成一个可执行目标文件。

l       可执行目标文件:包含二进制代码和数据,其可以加载器被拷贝至存储器并执行。

l       共享目标文件:也是一种可重定位目标文件,可在加载或运行时,被动态的加载到存储器并链接,俗称库。

 

声明:各个系统的目标文件格式不尽相同,但不管是哪种格式,基本的概念是相似的。下面我们集中说明一种在现代Unix系统中(如linux)经常使用的格式,Unix ELF(可执行和可链接格式)文件。

可重定位目标文件

万一你了解过PDF文件的格式,不用我多描述,你就会对ELF文件格式有个大概的猜测

(其实,文件格式都是这么来的)。

 

ELF头,包含的信息对我们理解链接过程来说无关紧要,无外乎一些大小信息,该目标文件的类型,该目标文件对应的机器类型,节头部表的节偏移等等。另外,每个节都有一个相应的表目,就是抽象记录各个节的一些信息,我们先不用关心这些。

我们应该关注的是里边的节的内容是什么,下面介绍一些应该关注的节:

.text:已编译程序的机器代码;

.rodata:只读数据,如printf语句中的格式串等等;

.data:重要的来了,已初始化的全局C变量。我们知道,局部C变量在运行时被保存在栈中,不出现在此范围内;

.bss:有个英文翻译叫:“better save space”,生动的描述了该节的意思,即未初始化的全局C变量,记住,在目标文件中,该节并不占磁盘空间,只是一个占位符。

.symtab:符号表,存放程序中定义和引用的函数和全局变量的信息,为链接的符号解析和重定位做好准备工作。

.rel.text:当链接器把该目标文件和其他文件结合时,.text节中的许多位置需要修改,如调用外部函数或者引用全局变量的指令都需要修改,修改后的结果位于该节。

.rel.data:被模块定义或引用的任何全局变量或者外部定义的函数的地址都需要修改,该节存储这些信息。

 

其它的节我们可以不用理会,无关紧要,就像我们在家吃饭时,美国洛杉矶的一条道路的红灯路口加了个摄像头来捕捉闯红灯的怪兽,事实上,并非这么回事,也没有这么夸张-_-。

接下来,我们就要讲到链接的两个主要任务了,符号解析和重定位。

符号解析

连接器解析符号引用的方法:将每个符号引用与相应的可重定位目标文件的符号表中的一个符号定义联系起来。这里的联系方法又涉及两种情况:

l       当引用和引用的定义在同一个模块中时,符号解析很容易直接匹配。

l       当引用的定义不在该模块中时,连接器会假设该符号引用在另外一个的模块中定义,生成一个链接器符号表表目,后继进行相应的解析匹配。

当然,如果你说还有一种情况,那就是引用一直找不到定义,就会输出一条错误信息并终止。

 

       有一个问题不知你想到没有,如果我们在多个目标文件中定义同一个全局符号,编译器并不一定会报错,就像我们将来结婚,怎么可能知道我们的儿子的名字会跟别人的不一样呢,其实,很多情况下是有同名的。因此,当你的儿子和那个同名的人第一次在一个班上上课时,当老师点到你儿子名字时,会有两个人应答,这种情况我们又不是没见过。而链接器使用一些规则来处理多处定义的符号,接下来,你该知道多处定义的全局符号会隐式导致一些我们无法预知的错误了吧!在此之前,我们要理解两个概念,强符号和弱符号,强符号即一些函数和已初始化的全局变量,而未初始化的全局变量是弱符号。

 

       规则如下:

l       不允许有多个强符号,否则报错;

l       如果有一个强,多个弱,那么只认强符号;

l       如果有多个弱,任选一个;

规则1:两个main的强类型符号,报错

 

       ------file1.c---------

        int main()

        {

               return 0;

        }

 

        ------file2.c---------

        int mian()

        {

               return 0;

        }

 

规则2:一个强,一个弱,强当然胜了

 

        ------file1.c---------

        int x = 2011; //强类型

        int main()

        {

               return 0;

        }

 

        ------file2.c---------

        int x; //弱类型

        int mian()

        {

               return 0;

        }

 

规则三:多个弱类型,不举例

 

        到目前为止,我们都是假设链接器读取一组可重定位目标文件,链接然后输出一个可执行目标文件。事实上,所有的编译系统都提供一种机制,即把相关的目标模块打包成一个单独的文件,称为静态库,可以用作链接器的输入。

        为什么用静态库:简单的说,静态库比以上介绍的方法一个明显的优点就是,节省空间,相对于把所有模块都链接到一个可执行文件的方法;不耗时,相对于把所有的可重定位目标文件(相关的或不相关的)都放在一个目录中再进行连接这种方法。事实上,静态库允许把可重定位目标文件组编译成库,在链接时,链接器知识拷贝被程序引用的目标模块,无关的模块走他们的路去。

关于链接器如何使用静态库来解析应用的,机制跟普通链接大同小异,都是通过遍历输入的目标文件来进行解析。以后会带上这个过程,先不讲这个,当然,你可以联想一下,我是想不出来,甚至就没有去联想的这个念头,哎!

重定位

解析引用已经完成,接下来就是重定位,重定位即把所有的输入模块的节合并,并为每个节分配运行时地址。其实就是分配地址。

l       重定位节和符号定义:节的地址赋值以及符号定义的地址赋值;链接器将所有相同类型的节合并为同一类型的新的聚合节。如,来自输入模块很多的.data节被合并成一个节,这个节成为输出的可执行文件的.data节。然后,链接器将运行时存储器地址赋给新的聚合节,赋给每个节,每个符号。这一步完成时,程序中的每个指令和全局变量都有唯一的运行时存储器地址(要的就是这个);

l       重定位节中的符号引用:有了符号定义的地址赋值,别忘了我们的引用啊,呵呵。

具体如何进行地址赋值,又是一个不小的话题,那我要做什么,你懂的-_-

可执行目标文件

我们的c程序,从一个ASCII文本文件,已经被转化为一个二进制文件,这个文件包含加载程序到存储器并运行它所需的所有信息,其实就是一个可执行文件,我们来看下ELF可执行文件的格式:

事实上,有很多部分跟可重定位文件格式一样:

ELF头部:其中包含程序的入口点,即程序运行时要执行的第一个指令的地址;

段头表:描述了可执行文件的组块被映射到连续的存储器段的映射关系;

.init节:定义一个_init函数,程序的初始化代码会调用它。

 

如何加载可执行文件:

     Unix$ ./p      //执行可执行目标文件p

每一个Unix程序都有一个运行时存储器映像。包括代码段,数据段,运行时堆,共享库,用户栈,都在特定的虚拟地址上。如图所示:

 

关于如何加载:Unix系统中的每一个程序都运行在一个进程上下文中,这个进程上下文有自己的虚拟地址空间,当shell运行一个程序时,父亲shell进程fork一个子进程,就是一个复制品,子进程通过系统调用execve启动加载器。加载器删除子进程中的虚拟存储数据段,并创建一组新的代码,数据,堆,栈段,新的堆和栈被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的组块,新的这个代码和数据段就被初始化为可执行文件中的内容。最后,加载器跳转到_start地址,它最终会调用应用的main函数。大概过程就是这样。

 

以上提到的很多内容涉及面很广,深入下去恐怕庞大无比,我也还不懂。

关于加载时动态链接共享库以及运行时动态链接共享库这两种情况以后再讲,今晚元宵节,早点回去吧。

 

                                                 --2011/02/17

                                                  

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值