分类: 嵌入式
2015-07-02 16:38:49
可能很多人和我一样,对操作系统中的动态加载技术“好像明白是怎么回事,但细细一想,又有很多东西想不清楚”,我想重要的一点是很少人去亲自设计过一个加载器(loader),自然其中的诸多细节就不了解。这里给大家分享一篇关于动态加载技术的文章,个人认为比网上很多只讲解elf格式,各种数据结构的学术型文章强不少。文章原网址:http://blog.csdn.net/jtj19850505/article/details/46626655 。 下面是正文:
网上有很多关于ELF格式介绍的文章。简单的说ELF文件是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储的标准文件格式。本文不描述ELF格式细节,而按照程序编译、链接、加载步骤中对ELF文件的处理流程来分析ELF文件。我不会去罗列一个ELF文件中有哪些数据结构,我只关心一个程序从编译到运行需要在ELF文件中保存写什么。ELF文件里具体保存了哪些内容?在我看来,ELF文件中保存的无非就是几类信息:数据集、指令集、指令对数据的引用关系以及指令集间的调用关系。
OBJ文件
ELF文件里具体保存了哪些内容?在我看来,ELF文件中保存的无非就是几类信息:数据集、指令集、指令对数据的引用关系以及指令集间的调用关系。程序是可供处理器执行的指令集合。一个函数在编译器编译后形成一个连续的指令块,而一个全局变量编译后生成一个数据块。一个源文件编译完成后,编译器将该文件所有函数的指令快拼接在一起形成一个text节,而将数据块拼接成data节,同样还会根据需要生成其它节,这里不作介绍。这些节拼接在一起形成obj文件,obj文件便是本文所介绍的第一种elf文件。然而简单的将各个函数生成的代码块和数据块拼接并不能描述函数之间的调用关系以及函数对全局变量的引用关系,因此,编译器会生成一些表保存它们之间的关系,这些表也以节的形式保存在obj文件中。这里重点介绍两类表:符号表和重定位表。
符号表负责记录本文件中可以被引用的符号信息,包括符号名称、位置、大小、类型。链接器或加载器根据名称在符号表中查找符号,获取符号地址等信息。至于地址信息如何被使用需依据重定位表中的表项决定。
重定位表记录本文件中尚未确定的指令和数据,这些指令和数据需在连接和加载时才能被确定和覆盖。重定位表项告诉链接器和加载器以下内容:某个位置的值还需要重新填充,怎么填充,需要用到符号表中哪个符号的值,怎么使用这个符号的值。
以函数为例,如源文件中有函数A和函数B,就会在符号表中生成两项,告诉链接器和加载器:“A和B在这里,这个obj文件中某某节,节内偏移是多少”。如函数A调用了函数B,则会在重定位表中增加一项,告诉链接器和加载器:“文件中有一条函数调用指令,指令位置在某个节的某个偏移,调用的目标函数名称是B”,链接器和加载器会去符号表中查找B符号的位置,填充到调用指令的目标地址字段(如果是相对跳转就需要计算出调用指令和被调用函数之间的偏移再填写,具体怎么处理由重定位项决定)。
内核模块文件
一个程序可能分布在多个源文件中,编译器为每个源文件生成一个obj文件,链接器负责将这些obj文件内容拼接成一个文件。拼接的方式有好几种,其中一种方式就是将所有obj文件中的同名节拼成一个节。这样就按名称拼成了几个大节如text节、data节。符号表和重定位表也被合并了,形成一个大符号表节和一个大重定位表节,再将几个合并后的节组成一个文件。这种文件便是本文要介绍的第二种ELF文件,linux和SylixOS中的内核模块文件(后缀名一般为.ko)就是这类文件。在这里链接器只是做了简单的拼接工作,将其它工作都交给加载器实现。
可执行ELF文件
事实上我们希望链接器能为我们做更多工作,因为链接是一劳永逸的,而加载是多次重复的。内核模块之所以将符号处理和重定位全部推迟到加载器执行,是因为内核加载器需要很多精确到符号和节的处理,这些我们将会在本系列文章的后续章节介绍。那么链接器还有哪些工作可以做呢?最容易想到的就是它至少应该处理完ELF文件内部的调用关系以及内部的变量引用。链接器扫描重定位表,检查每个重定位项是否可以内部解决,如果可以则处理完后将该表项删除。处理完成后,重定位表将只剩下对外部符号的引用关系表项。在支持多地址空间的操作系统中,链接器将ELF文件的加载地址也确定下来,这样就可以处理更多的重定位项,因为很多地址都确定了。链接器还会生成一些新的表,比如程序初始化函数表和销毁函数表,C++全局对象的构造函数通常在这个表里。至此便产生了本文所要阐述的第三中ELF文件格式,即可执行ELF文件。可执行ELF文件提供给加载器的不再是节,而是将节封装成更便于加载的段。节和段有什么不同呢,这么说吧,节告诉加载器:“这里有一个代码节,那里有个数据节,后面还有个重定位节,你自己去考虑怎么处理”。而段告诉加载器:“这个段可以加载,加载到哪个位置”。各个段加载完成后,可执行ELF文件还告诉加载器重定位表在哪个位置,初始化函数表在哪个位置,如此一来加载器的工作便简单多了。可执行ELF还会提供一个入口地址,通知加载器应该从哪个位置开始执行。这种ELF只能在多地址空间的操作系统如linux,windows系统下使用,因为加载地址在链接时就已经确定了,Linux为每个应用单独开辟一个地址空间,不会产生地址冲突,而在SylixOS这样的单地址空间系统中,所有应用程序运行在一个地址空间,很容易出现地址冲突。
位置无关ELF文件
上面提到的可执行ELF存在一个缺陷,就是再每个应用程序地址空间中只能加载一个这样的ELF,后续加载的ELF都需要考虑前面已加载的ELF,而在实际应用中这种情况是非常常见的,比如在linux中几乎每个程序都需要用到c库。为解决这个问题,连接器不再确定ELF的最终加载地址,为实现这一目的,编译器和链接器都需要参与进来,以gcc为例,程序在编译和链接时都需要加上-fPIC参数。我们把这类ELF称为位置无关ELF,也就是本文要讲的第四种ELF文件格式。要实现位置无关理论上说似乎很简单,只需将ELF本可以在链接时处理的一部分重定位项推迟到加载完所有ELF文件后统一处理。这是在没有考虑代码段共享的前提下,有代码段共享的系统要求位置无关ELF的代码段能在各个进程间共享,这就要求程序在重定位过程中代码段不会被修改。比如两个文件ELF_A和ELF_B相互引用到对方的符号,他们在进程PROCESS_A和PROCESS_B中的地址都不一样,二者的相对位置也不一样。我们先对PROCESS_A重定位,根据重定位表修改了ELF_A和ELF_B的代码段,PROCESS_A可以正常运行了。然后我们运行PROCESS_B,PROCESS_B也需要重定位,因此再次修改了ELF_A和ELF_B的代码段,这时PROCESS_A就不能正常运行了。可以使用linux的写时复制技术,但那样的话代码段共享就没有意义了。为解决这个问题,链接器为ELF文件生成一个GOT(Global OffsetTables)表,ELF所有对外部符号的引用都通过GOT表进行,代码段内所有对外部符号的访问都转换为对内部GOT表项的相对地址操作,GOT在加载时会被填写上对应外部符号地址,所有的重定位操作都只是修改了GOT表,不影响代码段。这个时候可以对GOT表进行写时拷贝了,因为它很小,不会导致大量的内存浪费。之所以在编译时就需要加上-fPIC参数,是因为编译器对跳转类代码(如函数调用)生成的指令已经不同了。SylixOS的应用程序和动态库都使用位置无关ELF文件格式,也支持代码段共享和写时拷贝。
静态库文件
这里再补充一下程序开发中经常要使用的库文件,库分为动态库和静态库。ELF动态库就是位置无关ELF格式的文件,这里再介绍静态库,静态库同样由obj文件拼接成,且拼接方式更加简单,甚至不用做节合并,只是简单到将所有obj文件打包到一起,有点类似于linux的tar命令。这就是本文要介绍的第五种ELF文件,其后缀通常为*.a。静态库文件并没有经过链接器处理,而是使用归档命令(ar)生成。它存在的目的只是为开发人员提供便利,这样他们不用去想他要去链接哪个文件,而是丢给链接器一个包,告诉它“你要的东西在这里面”,链接器需要符号时,自己去查找库中每个文件的符号表,如果存在就把文件提取出来进行链接。
SylixOS源码下载:git.sylixos.com