一、ELF简介
现在PC平台流行的可执行文件格式主要是Windows下的PE(portable Executable)和Linux的ELF(Excutable Linkable Format)。
编译器编译源代码后生成的文件叫做目标文件,从目标文件的结构上讲,
它是已经编译后的可执行文件格式,只是还没有链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。
简单的说,目标文件就是源代码编译后但未进行链接的那些中间文件(Winodws的.obj和Linux下的.o) ,它跟可执行文件的内容结构很相似,所以一般跟可执行文件格式一起采用一种格式存储。从某种意义上,可以把目标文件和可执行文件看成是一种类型的文件。在Windows下,称之为PE-COEF文件格式,在Linux下,称之为ELF文件。
另外,不光是可执行文件(.exe、elf)按照可执行文件格式存储,动态链接库.dll(windows)、.so(linux)以及静态连接库(.lib(windows)、.a(linux))文件都按可执行文件格式存储。
二、ELF结构
2.1一般目标文件将符号表、调试信息、字符串等一些链接时所须要的信息,以“节”(Section)的形式存储,有时候也叫“段”(Segment),通常不加区别。
-代码段(Code Section):存放源代码编译后的机器指令
-数据段(Data Section) : 存放全局变量和局部静态变量
- 数据段常见的名字:“.data”,".rodata",".comment",".bss"
- 未初始化的全部变量和局部变量放在“.bss”里,仅仅作为预留位置, 没有内容在文件中也不占据空间
- 只读数据段(.rodata) 和注释信息段(.comment)
2.2在ELF文件中实际存在(占据空间)的也就是“.text”".data" ".rodata"和“.comment”这4个段
2.3 调试工具
三、段表
与ELF文件中段有关的重要结构就是段表(Section HeaderTable),该表描述了ELF文件包含的所有段的信息,比如每个段的段名、长度、偏移、权限等属性。
readelf输出的结果就是ELF文件段表的内容。
Section Table的长度为0x1b8也就是440个字节,11个段描述符(10个有效+1个NULL),每个段描述符为40个字节。
四、动态链接
4.1 为了解决静态链接空间浪费和更新困难的问题,最简单的办法是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态地链接在一起。
简单的说,就是不对那些组成程序的目标文件进行链接,等到程序要运行的以后才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接的基本思想。
在Linux系统中,ELF动态链接文件被称为动态共享对象,简称共享对象,它们一般都是以".so"为拓展名的文件;而在Windows中,动态链接文件被称为动态链接库,他们通常就是平时常见的“.dll”为拓展名的文件。
4.2 libc简介
在Linux中,常用的C语言库运行库glibc动态链接形式保存在"/lib"目录下,文件名叫做“libc.so”,整个系统只保留一份C语言库的动态链接文件“libc.so”,而所有的C语言编写的、动态链接的程序都可以在运行时使用它。
当程序被装载时,系统的动态链接器 会将程序所需的动态链接库装载到进程的地址空间,并且将程序中所有未决议的符号绑定到相应的动态链接库中,并进行重定位工作。
动态链接是把链接的过程从本来的程序被装载前推迟到了装载的时候。
这样做虽然很灵活,但是性能受到影响。
这就引出了动态链接的优化方法。
4.3动态链接程序运行时地址空间分布
值得注意的是,动态链接模块装载地址是从地址0x00000000开始的。我们知道这个地址是无效地址,而实际装载地址是0xb7efc000。从中可以推断,共享对象的最终装载地址在编译时是不确定的,而是动态分配的。
原因是共享对象存在一些对象地址冲突的问题,可能会包含一些绝对地址的引用
与此不同的是,可执行文件往往是第一个被加载的文件,它可以选择一个固定空间的地址,比如Linux下一般都是0x0804000,windows下一般都是0x0040000.
4.4装载时重定位
基本思路是:在链接时,对所有绝对地址的引用不作重定位,而把这一步推迟到装载时再完成。一旦模块装载地址确定,即目标地址确定,那么系统就对程序中所有的绝对地址引用进行重定位。
假设函数foobar相对于代码段的起始地址是0x100,当模块被装载到0x10000000时,我们假设代码段位于模块的嘴开始,即代码段的装载地址也是0x10000000,那么我们就可以确定foobar的地位为0x1000100。这时候,系统遍历模块中的重定位表,把所有对foobar的地址引用都重定位至0x10000100。
4.5地址无关代码
装载时重定位解决了动态模块中有绝对地址引用的问题,但是又带了指令部分无法在多个进程间共享的问题。
具体想法就是把程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变。把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本。这种方案就是目前的地址无关代码(PIC)技术
具体方法:先分析模块中各种类型的地址引用方式,把共享对象模块中地址引用按照是否跨模块分为两类:模块内部引用和模块外部引用。
;按照不同的引用方式又可以分为指令引用和数据访问。
4.6全局偏移表(GOT)
4.6.1对于类型三,我们需要用到代码地址无关(PIC)技术,基本的思想就是把跟地址相关部分放到数据段里面。
ELF的做法是在数据段里建立一个指向这些变量的指针数据,称为全局偏移表(GOT),当代码需要引用该全局变量时,可以通过GOT中相对应的项间接引用。
如图,当指令需要访问变量b时,程序先会找到GOT,然后根据GOT中的变量所对应的项找到变量的目标地址。
由于GOT本身是放在数据段的,所以它可以在模块装载时被修改,并且每个进程都可以有独立的副本,相互不受影响。
可以使用objdump查看GOT的位置,以及GOT中变量的偏移
可以看到GOT在文件中的偏移是0x15d0
可以看到变量b在GOT中的偏移是8,相当于是第三项(每4个字节一项)
4.6.2 对于模块间调用和跳转,GOT中保存的是目标函数的地址,可以借助GOT中的项进行间接跳转。
方法:先得到当前指令地址PC,然后加上一个偏移地址得到函数地址在GOT中的偏移,然后一个间接调用
4.7 延迟绑定(PLT)
4.7.1 基本思想
动态链接以牺牲一部份性能为代价。PLT是另一种优化动态链接性能的方法。
4.7.2 具体做法
-动态链接器需要某个函数来完成地址绑定工作,这个函数至少要知道这个地址绑定发生在哪个模块 哪个函数,如lookup(module,function)。
在glibc中,lookup的函数真名叫做_dl_runtime_reolve()
- 当我们调用某个外部模块时,调用函数并不直接通过GOT跳转,而是通过一个叫做PLT项的结构来进行跳转,每个外部函数在PLT中都有一个相应的项,比如bar()函数在PLT中的项地址叫做bar@plt,具体实现
bar@plt:
jmp *(bar@GOT)
push n
push moduleID
jump _dl_runtime_resolve
第一条指令是一条通过GOT间接跳转指令,bar@GOT表示GOT中保存bar()这个函数的相应项。
但是为了实现延迟绑定,连接器在初始化阶段没有将bar()地址填入GOT,而是将“push n”的地址填入到bar@GOT中,所以第一条指令的效果是跳转到第二条指令,相当于没有进行任何操作。第二条指令将n压栈,接着将模块ID压栈,跳转到_dl_runtime_resolve。实际上就是lookup(module,function)的调用。
_dl_runtime_resolve()在工作完成后将bar()真实地址填入bar@GOT中。
一旦bar()解析完毕,再次调用bar@plt时,直接就能跳转到bar()的真实地址。
4.7.3实际实现
PLT的真正实现要更复杂些,ELF将GOT拆分成两个表“.got”和".got.plt",前者用来保存全局变量引用的地址,后者用来保存函数引用的地址。
也就是说,所有对于外部函数的引用被分离出来放到了“.got.plt”中
- 参考资料:《程序员的自我修养》