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