c语言书籍elf文件,C语言的本质(30)——C语言与汇编之ELF文件格式

ELF(Executable and Linking Format)文件格式是一个开放标准,各类UNIX系统的可执行文件都采用ELF格式,ELF是一种对象文件的格式,用于定义不一样类型的对象文件(Object files)的内容是什么、以及都以怎样的格式去存放这些内容。它有三种不一样的类型:shell

一、  可重定位的目标文件(Relocatable)编程

这是由汇编器汇编生成的 .o 文件。后面的连接器把一个或多个可重定位的目标文件做为输入,经连接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件(Shared object file)。咱们可使用 ar 工具将众多的 .o可重定位的目标文件归档(archive)成 .a 静态库文件。sass

二、  可执行文件(Executable)bash

咱们编译出的程序都是可执行文件。在 Linux 系统里面,存在两种可执行的东西。除了这里说的可执行文件,另一种就是可执行的脚本(如shell脚本)。注意这些脚本不是可执行文件,它们只是文本文件,可是执行这些脚本所用的解释器就是可执行文件,好比 bash shell 程序。工具

三、共享库(Shared Object)oop

这些就是所谓的动态库文件,也即 .so 文件。若是拿前面的静态库来生成可执行程序,那每一个生成的可执行程序中都会有一份库代码的拷贝。若是在磁盘中存储这些可执行程序,那就会占用额外的磁盘空间;另外若是拿它们放到Linux系统上一块儿运行,也会浪费掉宝贵的物理内存。若是将静态库换成动态库,那么这些问题都不会出现。布局

下面咱们分析上一篇中“求一组数的最大值的汇编程序”通过汇编以后生成的目标文件max.o和连接以后生成的可执行文件max的格式,从而理解汇编、连接和加载执行的过程。共享库之后再详细介绍。操作系统

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

5b65b43d07eb4f20a80e519c.html

左边是从汇编器和连接器的视角来看这个文件,开头的ELF Header描述了体系结构和操做系统等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置,Program Header Table在汇编和连接过程当中没有用到,因此是无关紧要的,SectionHeader Table中保存了全部Section的描述信息。右边是从加载器的视角来看这个文件,开头是ELF Header,Program Header Table中保存了全部Segment的描述信息,Section Header Table在加载过程当中没有用到,因此是无关紧要的。注意Section Header Table和Program Header Table并非必定要位于文件开头和结尾的,其位置由ELF Header指出,上图这么画只是为了清晰。orm

咱们在汇编程序中用.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,而后咱们逐段分析。

5b65b43d07eb4f20a80e519c.html

ELF Header中描述了操做系统是UNIX,体系结构是x86-64。SectionHeader Table中有8个节头(Section Header),在文件中的位置(或者叫文件地址)从216(0xD8)开始,每一个节(Section)64字节。这个目标文件没有Program Header。

5b65b43d07eb4f20a80e519c.html

从节头(Section Header)中读出各Section的描述信息,其中.text和.data是咱们在汇编程序中声明的Section,而其它Section是汇编器自动添加的。Addr是这些段加载到内存中的地址(咱们讲过程序中的地址都是虚拟地址),加载地址要在连接时填写,如今空缺,因此是全0。Off和Size两列指出了各Section的文件地址,好比.data从文件地址0x6d开始,一共0x38个字节,回去翻一下程序,.data中定义了14个4字节的整数,一共是56个字节,也就是0x38个。根据以上信息能够描绘出整个目标文件的布局。

max.o这个文件不大,咱们直接用hexdump工具把目标文件的字节所有打印出来看。

5b65b43d07eb4f20a80e519c.html

5b65b43d07eb4f20a80e519c.html

5b65b43d07eb4f20a80e519c.html

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

5b65b43d07eb4f20a80e519c.html

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

5b65b43d07eb4f20a80e519c.html

5b65b43d07eb4f20a80e519c.html

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

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

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

5b65b43d07eb4f20a80e519c.html

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

.symtab是符号表。Ndx列是每一个符号所在的Section编号,例如data_items在第3个Section里(也就是.data),各Section的编号见SectionHeader Table。Value列是每一个符号所表明的地址,在目标文件中,符号地址都是相对于该符号所在Section的相对地址,好比data_items位于.data段的开头,因此地址是0,_start位于.text段的开头,因此地址也是0,可是start_loop和loop_exit相对于.text段的地址就不是0了。从Bind这一列能够看出_start这个符号是GLOBAL的,而其它符号是LOCAL的,GLOBAL符号是在汇编程序中用.globl指示声明过的符号。

如今剩下.text段没有分析,objdump工具能够把程序中的机器指令反汇编(Disassemble),那么反汇编的结果是否跟原来写的汇编代码如出一辙呢?咱们对比分析一下。

5b65b43d07eb4f20a80e519c.html

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

如今咱们按前面的步骤分析可执行文件max,看看连接器都作了什么改动。

5b65b43d07eb4f20a80e519c.html

5b65b43d07eb4f20a80e519c.html

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

在Section Header Table中,.text和.data的加载地址分别改为了0x0804 8074和0x0804 90a0。.bss段没有用到,因此被删掉了。.rel.text段就是用于连接过程的,连接完了就没用了,因此也删掉了。

多出来的Program Header Table描述了两个Segment的信息。.text段和前面的ELF Header、Program Header Table一块儿组成一个Segment(FileSiz指出总长度是0x9e),.data段组成另外一个Segment(总长度是0x38)。VirtAddr列指出第一个Segment加载到虚拟地址0x0804 8000(注意在x86平台上后面的PhysAddr列是没有意义的),第二个Segment加载到地址0x0804 90a0。Flg列指出第一个Segment的访问权限是可读可执行,第二个Segment的访问权限是可读可写。最后一列Align的值0x1000(4K)是x86平台的内存页面大小。在加载时要求文件中的一页对应内存中的一页。

这个可执行文件很小,总共也不超过一页大小,可是两个Segment必须加载到内存中两个不一样的页面,由于MMU的权限保护机制是以页为单位的,一个页面只能设置一种权限。此外还规定每一个Segment在文件页面内偏移多少加载到内存页面仍然偏移多少,这样规定是为了简化连接器和加载器的实现。

原来目标文件符号表中的Value都是相对地址,如今都改为绝对地址了。此外还多了三个符号__bss_start、_edata和_end,这些是在连接过程当中添进去的,加载器能够利用这些信息把.bss段初始化为0。

再看一下反汇编的结果:

5b65b43d07eb4f20a80e519c.html

指令中的相对地址都改为绝对地址了。其实只是反汇编的结果不一样了,指令根本没改。为何不用改指令就能跳转到新的地址呢?由于跳转指令中指定的是相对于当前指令向前或向后跳多少字节,而不是指定一个完整的内存地址,内存地址有32位,这些跳转指令只有16位,显然也不可能指定一个完整的内存地址,这称为相对跳转。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值