解析目标文件

来源:互联网 发布:dpp软件下载 编辑:程序博客网 时间:2024/06/05 06:54


  1. PC平台上流行的可执行文件格式主要是windows下的PE(Portable Executable)和Linux下的ELF(Executable Linkable Format),他们都是COFF(common file format)格式的变种。
  2. 可执行文件(windows下.exe和Linux下的ELF可执行文件)、动态链接库(DLL,Dynamic Linking Library)(windows下的.dll和Linux下的.so)、静态链接库(Static Linking Library)(windows下的.lib和Linux下的.a)文件都是按照可执行文件格式存储。
  3. 目标文件中的内容至少有编译后的机器指令代码、数据,还有链接时需要的一些信息,如符号表、调试信息、字符串等。以“段”的形式存储。
  4. 代码段(.text或.code):程序源代码编译后的机器指令;
  5. 数据段(.data):放置全局变量和局部静态变量;
  6. .bss段:放置未初始化的全局变量和局部静态变量;
  7. 程序指令和数据分开存放的好处:
  • 程序被装载后,数据和指令分别被映射到两个虚存区域。数据区域对于进程来说可读写,指令区域对于进程来说是只读的,所以两个虚存区域的权限可以被分别设置成可读写和只读,这样可以防止程序的指令被有意或者无意的修改;
  • 程序的指令和数据分开存对CPU的缓存命中率提高有好处;
  • 当系统中运行多个改程序的副本时,他们的指令都是一样,因此在内存中只须保存一份该程序的指令部分。当然每个副本进程的数据区域是不一样的,他们是进程私有的。

挖掘目标文件SimpleSection.o

1.    程序代码清单

只编译不链接此文件:

$ gcc –c SimpleSection.c

利用binutils的工具objdump查看object内容的结构:

$ objdump –h SimpleSection.o

参数-h就是把ELF文件的各个段的基本信息打印出来。结果如下:

除了最基本的代码段、数据段、BSS段之外,SimpleSection.o还有只读数据段(.rodata)、注释信息段(.comment)、堆栈提示段(.note.GNU-stack)、eh_frame段。

从上图可以理解,段的长度(Size)和段所在的位置(File Offset),“CONTENTS”表示该段在文件中存在,“ALLOC”表示实际上ELF文件中不存在的内容。各段在ELF中的结构如下图所示。

  

$size SimpleSection.o

用于查看ELF文件的代码段、数据段和BSS段的长度。dec表示三段长度和的十进制,hex表示长度和的十六进制。

2.  代码段

objdump的“-s”参数可以将所有段的内容以十六进制的方式打印出来,“-d”参数可以将所有包含指令的段反汇编。

$ objdump –s –d SimpleSection.o

最左面一列是偏移量,中间4列是十六进制内容,最右面的一列是.text段的ASCII码。

3.  数据段和只读数据段

.data段保存的是那些已经初始化了的全局静态变量和局部静态变量。

.rodata段存放的是只读数据,一般是程序里面的只读变量,如const修饰的变量和字符串常量。

$objdump –x –s –d SimpleSection.o

可以看出.data段里的前四个字节,从低到高分别是0x54、0x00、0x00、0x00。这个值刚好是global_init_varable,即十进制84。

4.  BSS段

.bss段存放的是未初始化的全局变量和局部静态变量。如代码中的global_uninit_var和static_var2就是存放在.bss段,更准确的说法是.bss段为它们预留了空间。有些编译器会将全局的未初始化变量存放在目标文件的.bss段,有些则不放,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss段分配空间。

$objdump –x –s –d SimpleSection.o

5.  其他段

ELF文件结构

ELF目标文件格式的最前端是ELF文件头(ELF Header),包含了描述整个文件的基本属性,如ELF版本、目标机器型号、程序入口地址等。

ELF文件中与段有关的重要结构就是段表(Section Header Table),该表描述了所有段的信息,如每个段的段名、段的长度、在文件中的偏移、读写权限和段的其他属性。

ELF中的其他辅助结构,如字符串表、符号表等。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

1.         ELF文件头

$readelf –h SimpleSection.o

从上图可以看出,ELF文件头中定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的入口和长度、段表的位置和长度、段的数量等。

ELF文件头结构及相关常熟被定义在“/usr/include/elf.h”里,因为ELF文件在各种平台下都通用,ELF文件有32位版本和64版本。分为为 “Elf32_Ehdr”和 “Elf64_Ehdr”。

“elf.h”使用typedef定义了一套自己的变量体系,如下图。

以32位版本的文件头结构“Elf32_Ehdr”为例,其定义如下:

复制代码
  1 typedef struct{  2     unsigned char e_ident[16];  3     Elf32_Half e_type;  4     Elf32_Half e_machine;  5     Elf32_Word e_version;  6     Elf32_Addr e_entry;  7     Elf32_Off e_phoff;  8     Elf32_Off e_shoff;  9     Elf32_Word e_flags; 10     Elf32_Half e_ehsize; 11     Elf32_Half e_phentsize; 12     Elf32_Half e_phnum; 13     Elf32_Half e_shentsize; 14     Elf32_Half e_shnum; 15     Elf32_Half e_shstrndx; 16 }Elf32_Ehdr; 17     
复制代码

各个成员的含义如下:

  • ELF魔数

最开始的4个字节是所有ELF文件都必须相同的标识码,分别为0x7F、0x45、0x4c、0x46,第一个字节对应的ASCII字符里的DEL控制符,后面的3个字符刚好是ELF这三个字符的ASCII码。这4个字节被称为ELF文件的魔数,几乎所有的可执行文件格式的最开始几个字节都是魔数。

  • 文件类型

即前面提到过的3种ELF文件类型,每个文件类型对应一个常量。系统通过这个常量来判断ELF文件的真正文件类型,而不是通过文件的扩展名。

2.  段表

段表(Section Header Table)就是保持ELF文件各段基本属性的结构。编译器、链接器、装载器都是依靠段表来定位和访问各个段的属性的。使用readelf工具来查看ELF文件段的结构。

$readelf –S SimpleSection.o

段表的结构比较简单,它是以“Elf32_Shdr”结构体为元素的数组,数组元素的个数等于段的个数。“Elf32_Shdr”也被称为段描述符(Section Descriptor)

Elf32_Shdr各成员的含义如下:

至此,才把SimpleSection的所有段的位置和长度分析清楚,如下图所示。段表Section Table长度为0x208,即520个字节,包含了13个段描述符。每个段描述符为4×10=40Bytes。

3.  重定位表

链接器在处理目标文件时,需要对目标文件中的某些部位进行重定位,即代码段和数据段中的那些对绝对位置的引用的位置,如.rel.text就是针对.text段的重定位表,因为.text段中至少有一个绝对地址的引用,那就是printf函数的调用。

4.  字符串表

ELF文件中用到了很多字符串,如段名、变量名等,由于字符串的长度往往不定,因此常把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。

一般字符串在ELF文件中也以段的形式保存,常见的段名如.strtab和.shstrtab。字符串表(.strtab)保存普通的字符串,段表字符串表(.shstrtab)保存段表中用到的字符串,最常见的就是段名。


/////////////////////////////////////////////////////////////////////////////////////

对于text和data段而言,文件偏移和逻辑地址取模4KB后都是相等的。但是:  

1. text的第一个页面包含了ELF header,program header table以及其他的信息;  

2. text的最后一个页面包含了data的开始部分数据的拷贝;  

3. data的第一个页面包含了text的末尾数据的拷贝;  

4. data的最后一个页面也许包含了和运行进程不相关的文件信息;  

理论上讲,系统对待每个段的内存权限都是相互独立的。段地址不得不调整来确保地址空间中的每个逻辑页面都有自己的权限;在上面的例子中,包含了text结尾和data开始的区域将要被映射两次:一次就是包含了text和data开始部分,另一个就是text末尾部分和data;data段的末尾还需要对为初始化数据的特殊处理,系统通常将其清零。

 

程序编译后生成的目标文件至少含有三个段,分别为:.text、.data和.bss。这三个段的大致结构图如下所示:

 


 

其中.text即为代码段,为只读。.bss段包含程序中未初始化的全局变量和static变量。

 

data段包含三个部分:heap(堆)、stack(栈)和静态数据区。

 

当程序在执行时动态分配空间(C中的malloc函数),所分配的空间就属于heap。其概念与数据结构中“堆”的概念不同。

 

stack段存放函数内部的局部变量、参数和返回地址,其在函数被调用时自动分配,访问方式就是标准栈中的LIFO方式。(因为函数的局部变量存放在此,因此其访问方式应该是栈指针加偏移的方式,否则若通过push、pop操作来访问相当麻烦)

 

data段中的静态数据区存放的是程序中已初始化的全局变量、静态变量和常量。

 

代码段(text):代码段是用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存种的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。

数据段(data):数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配[1]的变量和全局变量

BSS段:BSS段包含了程序中未初始化全局变量,在内存中bss段全部置零。BSS是block started by symbol的缩写。因为未初始化的变量没有对应的值,所以并不需要存储在可执行对象中。但是因为C标准强制规定未初始化的全局变量要被赋予特殊的默认值(基本上是0值),所以内核要从可执行代码装入变量(未赋值的)到内存中,然后将零页映射到该片内存上,于是这些未初始化变量就被赋予了0值。这样做避免了在目标文件中进行显式地初始化,减少空间浪费


0 0
原创粉丝点击