一、格式
一份 C 代碼經過編譯后,可以生成能直接運行的二進制文件,在不同操作系統上這些二進制文件有不同的特征,在 Windows 上通常后綴為 .exe,在 Linux 上通常沒有后綴。除此之外,這些二進制文件在內部數據的組織和結構上也有很大的區別,針對 Windows 的有 PE 格式,類 Unix 系統的有 elf 格式。
1.ELF 格式
不同可執行文件的數據組織和結構雖然不同,但它們對數據的基本組織方式都符合這樣一個特征:使用統一的頭部來保存可執行文件的基本信息,而其他數據則按照功能被划分在了以 section 或 segment 形式組織的一系列單元中。通過 file 命令可以看出這是一個共享目標文件,老版 gcc 生成的是可執行文件,新版默認啟用 PIE 功能,讓程序可以在任意地址加載,減少了系統攻擊的風險,所以現在生成的是共享目標文件:
可以看出,它確實開了 pie:
2.ELF頭
readelf 命令可以查看可執行文件的內部組成結構,用來獲取可執行文件的信息,比如 readlf -h:
魔數第一個字節固定為 0x7f;二到三字節表示 ELF 的 ASCII 碼;第五個字節表示操作系統位數,0x1表示 32 位,0x2 表示 64 位;第六個字節表示字節序;第七個字節表示版本號。Data 字段是數據存儲形式,可以看出它是小端存儲模式;Entry point address 是程序的入口地址,也就是執行程序時執行的第一條指令的地址。
3.ELF section頭
通過 readelf -S 命令獲取 ELF 文件頭的信息,-E 表示按照一定規則擴展,-A 表示顯示找到的數據的前一行和后一行:
上圖所示的四個段分別是代碼段,只讀數據段,數據段(存放已初始化的全局變量),.bss 存放未初始化的全局變量或局部變量。可以通過 objdump 來驗證:
可以看出 09f0 確實存放了右圖代碼中的只讀數據,同理可以看到初始化的全局變量,也就是 .data 的值,左圖是十六進制,0x14 對應右圖的 20:
在 ELF 格式中,這些 section 組成了組成了描述該 ELF 文件內容的靜態視圖,而靜態視圖的重要作用是完成鏈接,鏈接將不同的類型的 ELF 文件相互整合,並最終生成可執行文件。
4.ELF Program頭
除了由 section 組成的靜態視圖外,還有很多 secment 組成了組成了描述可執行文件的動態視圖。segment 指定了應用程序在實際運行時,應如何在進程的 VAS 內部組織數據,可以通過 readelf -l 命令顯示可執行文件的 segment 情況,這個命令顯示出的第一部分是各個 segment 的信息:
第二部分顯示了 segment 和 section 的對應關系,其中第三個 segment 對應了 .text 和 .rodata 等 section ,第四個 segment 對應了 .data 和 .bss 等 section:
segment 對應的頭叫做 program 頭,其中包含各個 segment 的類型、偏移地址、對齊情況等信息。LOAD 類型的 segment 在程序運行時會真正被載入到進程的 VAS 中,其余的 segment 主要用於輔助程序的正常運行(比如進行動態鏈接)。
關於 elf 文件格式的更多信息可以參考這個文檔 和 Linux 手冊。
5.ELF文件類型
ELF 作為一種文件格式,可以被四種類型的文件使用,分別是可重定位文件、可執行文件、共享目標文件和核心轉儲文件,如下圖所示:
這四種 ELF 文件類型,雖然名稱各不相同,但其內部的數據組織方式都遵循同樣的 ELF 文件格式標准。由於功能不同,所以內部的組成有些差異。
1.比如可重定位文件,也就是 .o 文件,用於大型項目的增量式開發。將一些功能代碼編譯成一個可重定位文件,需要這個功能的程序只需要鏈接這個可重定位文件就可以實現這些功能了,這樣做的好處是當這些功能發生變化時,可以只編譯功能發生變化的代碼。可重定位文件只有 section 的相關信息,沒有 Program 頭等用於支持運行的 ELF 結構,因此該類型的文件無法直接運行。需要經過靜態鏈接生成含有 Program 頭的可執行文件。