可執行文件的數據結構一般都很複雜和繁瑣。緣由在於程序在加載到內存中執行時須要通過一系列很是複雜的步驟。例如要計算數據或代碼被加載到虛擬內存時的位置,計算重定向數值,實現不一樣代碼的連接等。
本節咱們一點一滴的瞭解段的數據格式和做用,這對咱們後面瞭解Linux系統如何加載運行程序,並掌握相關的高級hack技術有很是重要的做用,首先咱們看段頭對應的數據結構,它用於描述ELF文件中某個段的基本特徵:
typedef struct {
uint32_t sh_name; #段名
uint32_t sh_type; #段的類型
uint64_t sh_flags; #段標誌位
uint64_t sh_addr; #段被加載到內存中的位置
uint64_t sh_offset; #段對應數據在ELF文件中的偏移
uint64_t sh_size; #段的大小
uint64_t sh_link; #與該段有關係的其餘段對應的段頭在段頭表中的下標
uint32_t sh_info; #與該段有關的信息
uint64_t sh_addralign; #段是否須要字節對齊、
uint64_t sh_entsize; #若是段含有表結構,該字段對應表中每一項的大小
}Elf64_Shdr;
用於描述段性質的段頭數據結構以表格的方式存儲在ELF文件中。表中每一項就對應上面描述的數據結構。該結構中的第一個字段用於指向段的名稱。名稱固然得對應字符串,但sh_name倒是一個四字節整形,那是由於全部字符串都被存儲在字符串段中,這個段裏面的數據全是以0結尾的字符串組成,sh_name表示在字符串段裏,第幾個字符串對應當前段的名稱。
字段sh_type用於描述段的類型,某些特定類型的段對應特定的數據結構,操做系統的加載器經過解讀該字段,瞭解段的類型後就能夠知道如何讀取段的內容。常見的段類型有SHT_PROGBITS,這種段包含機器指令或者常量數據,它就是一堆數據的集合,所以沒有特定結構。
SHT_SYMTAB表示靜態符號表,SHT_DYNSYM表示動態符號表,這些段有特定的數據結構,他們會被調試器或鏈接器讀取。SHT_REL或SHT_RELA表示該段用於重定向,連接器須要深刻讀取這些段的信息。
sh_flag字段用於描述段的屬性。若是他的值位SHF_WRITE,表示該段的內容在程序運行時能夠被修改,SHF_ALLOC表示該段的數據在程序運行時動態加載到內存中,SHF_EXEINSTR表示該段包含了能夠被執行的機器指令。其餘字段在後面須要時再瞭解,下面咱們看段的內容和做用。
使用命令readelf —section —wide a.out咱們能夠獲得以下內容:
上圖展示可執行文件各個段的信息,其中若干段須要咱們多瞭解。咱們看.init和.fini這兩段,其類型爲PROGBITS,這代表這兩個段的內容爲可執行指令。.init段包含了程序在執行前所須要的初始化操做,使用C語言編程時入口是main,這部分代碼就是main在執行前所須要運行的指令。當程序運行結束後,.fini中對應的代碼會被執行已完成資源回收等操做。
很重要的一個段就是.text,它包含了咱們編寫的代碼編譯成的二進制指令都在該段中。若是代碼使用gcc編譯,那麼編譯器在編譯代碼時會自動插入一些通用函數,例如_start, reigser_tm_clones等,可使用命令將這些指令反編譯出來:objdump -M intel -d a.out:
上圖就是_start函數對應的代碼指令。順序看下來有一條語句mov rdi, 400526,後面數值就是main函數的入口地址,把該地址放入rdi寄存器目的是爲了調用main函數時傳遞調用參數,接下來語句call libc_start_main@plt,該函數屬於plt段,是一個連接庫裏面的函數,在這個函數裏面會使用call指令調用咱們所寫的main函數,後面咱們會詳細研究這些函數。
任何存儲代碼指令的段都不可寫,相似的段還有.rodata,它用來存儲只讀數據。.data段用於存儲程序默認初始化數據,所以它可寫,也就是裏面的數據能夠修改。.bss段用於存儲那些沒有在代碼中初始化的變量,在程序加載後,系統會爲該段內的數據分配內存。
系統在加載ELF文件執行時,代碼中有很多函數對應的調用地址尚未肯定。例如經常使用的C函數像memset等,這些函數因爲位於共享連接庫中,所以他們對應的虛擬地址編譯器不知道,這就須要系統在代碼調用這些函數時纔去肯定他們的具體地址,這種技術也叫有延遲綁定。
ELF文件中幫助系統進行延遲綁定的有兩個段分別爲.plt和.got。咱們使用以下命令來反編譯.glt以便看看它的結構和內容: objdump -M intel —section .plt -d a.out
上圖中咱們能夠看到puts,它是linux系統中經常使用的將信息輸出到控制檯的函數。若是咱們在代碼中調用puts函數時,實際上編譯器會先調用上圖裏面的puts@plt這部分的指令。咱們看上圖中第一段裏面的指令jumpq *0x200c12(%rip),它的意思是將數值0x200c12加上rip寄存器中的值,rip存儲當前指令下一條指令的地址,也就是400406,相加後結果爲0x601018,而後讀取所在地址的數值,咱們經過指令objdump -M intel -section .got.plt -d a.out來看對應地址的數值:
能夠看到對應地址的數值是06,因而將當前地址加上06獲得下一條指令的地址,也就是400406,這其實就是jumpq語句下一條指令的地址,也就是代碼繞了一個大彎後回到了下面指令,而後執行指令push 0,將數值0壓到堆棧上,接着jumppq跳轉到地址0x4003f0也就是最上面代碼的入口。而後又執行指令pushq 0x200c12(%rip),這個地址實際落在段.got裏面,而後又執行語句jump *0x200c14(%rip),後面對應的地址其實也在.got段裏。
當這些代碼執行時,動態連接庫就會修改0x200c12(%rip)地址存儲的數值,使得該數值加上jumpq指令所在地址後指向puts函數的虛擬地址,因而在真正執行指令jumpq *0x200c12(%rip)時就不會繞個大彎轉回下面的push指令,而是直接跳轉到puts函數,這個過程很繁瑣,下一節咱們再看爲什麼要使用.got段來實現動態連接。
本文分享自微信公衆號 - Coding迪斯尼(gh_c9f933e7765d)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。linux