ELF文件

2018-06-18 04:16:43来源:未知 阅读 ()

新老客户大回馈,云服务器低至5折

ELF文件格式是一个开发标准,各种UNIX系统的可执行文件都采用ELF格式,它有三种不同的类型:

  • 可重定位的目标文件
  • 可执行文件
  • 共享库

现在分析一下上一篇文章中经过汇编之后生成的目标文件max.o和链接之后生成的可执行文件max的格式,从而理解汇编、链接和加载执行的过程。

一、目标文件

ELF文件格式提供了两种不同的视角,在汇编器和链接器看来,ELF文件是由Section Header Table描述的一系列Section的集合,而执行一个ELF文件时,在加载器看来它是由Program Header Table描述的一系列Segment的集合,如下图所示:

左边是从汇编器和链接器的视角来看这个文件,开头的ELF Header描述了体系结构和操作系统等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置,Program Header Table在汇编和链接过程中没有用到,所以是可有可无的,Section Header Table中保存了所有Section的描述信息。右边是从加载器的视角来看这个文件,开头是ELF Header,Program Header Table中保存了所有Segment的描述信息,Section Header Table在加载过程中没有用到,所以是可有可无的。注意Section Header Table和Program Header并不一定要位于文件开头和结尾的,其位置由ELF Header指出。

我们在汇编程序中用.section声明的Section会成为目标文件中的Section,此外汇编器还会自动添加一些Section(比如符号表)。Segment是指程序运行时加载到内存的具有相同属性的区域,由一个或多个Section组成,比如有两个Section都要求加载到内存后可读可写,就属于同一个Segment。有些Section只对汇编器和链接器有意义,在运行时用不到,也不需要加载到内存,那么句不属于任何Segment。

目标文件需要连接器做进一步处理,所以一定有Section Header Table;可执行文件需要加载运行,所以一定有Program Header Table;而共享库既要加载运行,又要在加载时做动态链接,所以既有Section Header Table又有Program Header Table。

下面用readelf工具读出目标文件max.o的ELF Header和Section Header Table,然后我们逐段分析。

 

ELF Header中描述了操作系统是UNIX,体系结构是80386.Section Header Table中有8个Section Header,在文件中的位置(或者叫文件地址)从180(0xc8)开始,每个40字节,共320字节,到文件地址0x1f3结束。这个目标文件没有Program Header。

从Section Header中读出各Section的描述信息,其中.text和.data是我们在汇编程序中声明的Section,而其他Section是汇编器自动添加的。Addr是这些段加载到内存中的地址(程序中的地址都是虚拟地址),加载地址要在链接时填写,现在空缺,所以是00000000.Off和Size两列指出了各Section的文件地址,比如.data从文件地址0x60开始,一共0x24个字节,我们在max程序中定义了9个4字节的整数,一共是36个字节,十六进制则为0x24个。根据以上信息可以描绘出整个目标文件的布局:

我们用hexdump工具把目标文件的字节全部打印出来看。

左边一列是文件中的地址,中间是每个字节的16进制表示,右边是把这些字节解释成ASCII码所对应的字符。中介有一个*号表示省略的部分全是0。.data段对应的是这一块:

这一段将来要原封不动地加载到内存中。(蓝色线划掉的不属于.data段)(03 00 00 00实际上读的时候是00 00 00 03, 2d 00 00 00读的时候是 00 00 00 2d;可以看到是倒过来读的,下面会讲到)

.shstrtab和.strtab这两个Section中存放的都是ASCII码:

...

 

可见.shstrtab中保存着各Section的名字,.strtab中保存着程序中用到的符号的名字。每个名字都是以'\0'结尾的字符串。

C语言的全局变量如果在代码中没有初始化,就会在程序加载时用0初始化。这种数据属于.bss段,在加载时它和.data段一样是可读可写的数据,但是在ELF文件中.data段需要占用一部分空间保存初始值,而.bss段则不需要。也就是说.bss段在文件中值占一个Section Header而没有对应的Section,程序加载时.bss段占多大内存空间在Section Header中描述。

 

我们继续分析readelf输出的最后一部分,是从.rel .text和.symtab这两个Section中读出的信息

.rel .text告诉链接器指令中的哪些地方需要重定位。

.symtab是符号表。Ndx列是每个符号所在的Section编号,例如data_items在第三个Section里(也就是.data)各Section的编号见Section Header Table。Value列是每个符号所代表的地址,在目标文件中。符号地址都是相对于该符号所在Section的相对地址,比如data_items位于.data段的开头,所以地址是0。从Bind这一列可以看出_start这个符号是GLOBAL的,而其他符号是LOCAL的。

现在只剩下.text段没有分析。objdump工具可以把程序中的机器指令反汇编,那么反汇编的结果是否跟原来写的汇编代码一模一样呢?对比:

左边是机器指令的字节,右边是反汇编结果。显然所有的符号都被替换成地址了,比如je 23,注意没有加$的数表示内存地址,而不表示立即数。这条指令后面的<loop_exit>并不是指令的一部分,而是反汇编器从.symtab和.strtab查到的符号名称,写在后面是为了有更好的可读性。目前所有的跳转指令和内存访问指令(mov 0x0(,%edi,4), %eax)中的地址都是符号的相对地址,下一步链接器要修改这些指令,把其中的地址都改成加载时的内存地址,这些指令才能正确执行。

 

二、可执行文件

 

我们按上一节的步骤分析可执行文件max,看看链接器都做了什么改动

在ELF Header中,Type改成了EXEC,由目标文件变成可执行文件了,Entry point改成了0x8048074(这是_start符号的地址),还可以看出多了两个Program Header,少了两个Section Header。

在Section Heade Table中 .text和.data的加载地址分别改成了0x8048074和0x80490a0。.bss段没有用到,所以被删掉了。.rel .text段就是用于链接过程的,链接完了就没用了,所以也删掉了。

多出来的Program Header Table描述了两个Segment的信息。.text段和前面的ELF Header、Program Header Table一起组成一个Segment(FileSize指出总长度是0x9e), .data段组成另一个Segment(总长度是0x38)VirtAddr列指出第一个Segment加载到虚地址0x08048000,第二个Segment加载到地址0x080490a0。Fig列指出第一个Segment的访问权限是可读可执行,第二个Segment的访问权限是可读可写的。最后一列Align的值0x1000(4K)是x86平台的内存页面大小。在加载时要求文件中的一页对应内存中的一页,对应关系如下:

这个可执行文件很小,总共也不超过一页大小,但是两个Segment必须加载到内存中两个不同的页面,因为MMU的权限保护机制是以页为单位的,一个页面只能设置一种权限。此外还规定每个Segment在文件页面内偏移多少加载到内存页面仍然偏移多少,比如第二个Segment在文件中的偏移是0xa0,在内存页面0x0804 9000中的偏移仍然是0xa0,所以是从0x0804 90a0开始,这样规定是为了简化链接器和加载器的实现。从上图也可以看出 .text 段的加载地址应该是 0x0804 8074 ,也正是 _start 符号的地址和程序的入口地址。原来目标文件符号表中的 Value 都是相对地址,现在都改成绝对地址了。此外还多了三个符号 __bss_start 、 _edata 和 _end ,这些是在链接过程中添进去的,加载器可以利用这些信息把 .bss 段初始化为0。

再看一下反汇编的结果:

指令中的相对地址都改成绝对地址了。我们仔细检查一下都改了哪些地方。首先看跳转指令,原来目标文件的指令是这样:

......
11: 74 10 je 23 <loop_exit>
......
1d: 7e ef jle e <start_loop>
......
21: eb eb jmp e <start_loop>
......

现在改成了这样:

......
8048085: 74 10 je 8048097 <loop_exit>
......
8048091: 7e ef jle 8048082 <start_loop>
......
8048095: eb eb jmp 8048082 <start_loop>
......

改了吗?其实只是反汇编的结果不同了,指令根本没改。为什么不用改指令就能跳转到新的地址呢?因为跳转指令中指定的是相对于当前指令向前或向后跳多少字节,而不是指定一个完整的内存地址,内存地址有32位,这些跳转指令只有16位,显然也不可能指定一个完整的内存地址,这称为相对跳转。
再看内存访问指令,原来目标文件的指令是这样:

......
5: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax
......
14: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax
......

现在改成了这样:

......
8048079: 8b 04 bd a0 90 04 08 mov 0x80490a0(,%edi,4),%eax
......
8048088: 8b 04 bd a0 90 04 08 mov 0x80490a0(,%edi,4),%eax
......

指令中的地址原本是0x0000 0000,现在改成了0x0804 09a0(注意是小端字节序)。那么链接器怎么知道要改这两处呢?是根据原来目标文件中的 .rel.text 段提供的重定位信息来改的:

......
Relocation section '.rel.text' at offset 0x2b0 contains 2 entries:
Offset   Info     Type     Sym.Value  Sym. Name
00000008 00000201 R_386_32 00000000   .data
00000017 00000201 R_386_32 00000000   .data
......

第一列 Offset 的值就是 .text 段需要改的地方,在 .text 段中的相对地址是8和0x17,正是这两条指令中00 00 00 00的位置。

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:Hanoi Tower 汉诺塔问题 /c /python

下一篇:分治法与归并排序