解析目标文件

最近在看《程序员的自我修养》,颇有体会,故化繁为简,整理书中部分内容,作为学习笔记。

  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)保存段表中用到的字符串,最常见的就是段名。

 

参考资料:《程序员的自我修养——链接、装载与库》

Jacky Liu

posted @ 2013-01-28 21:46  阳光守望者  阅读(4299)  评论(2编辑  收藏  举报