解析目標文件


最近在看《程序員的自我修養》,頗有體會,故化繁為簡,整理書中部分內容,作為學習筆記。

  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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM