解析目标文件
最近在看《程序员的自我修养》,颇有体会,故化繁为简,整理书中部分内容,作为学习笔记。
- PC平台上流行的可执行文件格式主要是windows下的PE(Portable Executable)和Linux下的ELF(Executable Linkable Format),他们都是COFF(common file format)格式的变种。
- 可执行文件(windows下.exe和Linux下的ELF可执行文件)、动态链接库(DLL,Dynamic Linking Library)(windows下的.dll和Linux下的.so)、静态链接库(Static Linking Library)(windows下的.lib和Linux下的.a)文件都是按照可执行文件格式存储。
- 目标文件中的内容至少有编译后的机器指令代码、数据,还有链接时需要的一些信息,如符号表、调试信息、字符串等。以“段”的形式存储。
- 代码段(.text或.code):程序源代码编译后的机器指令;
- 数据段(.data):放置全局变量和局部静态变量;
- .bss段:放置未初始化的全局变量和局部静态变量;
- 程序指令和数据分开存放的好处:
- 程序被装载后,数据和指令分别被映射到两个虚存区域。数据区域对于进程来说可读写,指令区域对于进程来说是只读的,所以两个虚存区域的权限可以被分别设置成可读写和只读,这样可以防止程序的指令被有意或者无意的修改;
- 程序的指令和数据分开存对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