Linux 核心簡介中斷機制行程管理記憶體管理檔案系統輸出入系統目的檔動態連結相關資訊相關網站相關文獻最新修改訊息相關網站參考文獻最新修改簡體版English |
Linux 的目的檔為了效能的緣故,目的檔通常不會儲存為文字格式,而是儲存為二進位格式。像是 DOS 當中的 .com 檔案,Windows 當中的 .exe 檔案,與 Linux 早期的 a.out格式,還有近期的ELF格式,都是常見的目的檔格式。 為了讓讀者更清楚目的檔的格式,在本節中,我們將以Linux 早期的 a.out ,與近期的 ELF 格式,作為範例,以便詳細說明目的檔的格式。 圖 1. 兩種目的檔的格式 – a.out 與 ELF 之比較 目的檔格式 a.out在早期的 Linux,像是 Linux 0.12 版,採用的是較簡單的目的檔格式,稱為 a.out。這是由於 UNIX與 Linux 的預設編譯執行檔名稱為 a.out 的原因。圖 1 顯示了a.out 的檔案格式 ,並且與目前 Linux 所使用的 ELF 格式進行對比。 目的檔 a.out 的格式相當簡單,總共分為 7 的段落,包含三種主要的資料結構,也就是 1. 檔頭結構 (exec)、2. 重定位結構 (relocation_info)、3. 字串表結構 (nlist)。這三個結構的定義如下程式所示。 程式片段 1 : 目的檔 a.out 的資料結構 (以C語言定義)
請讀者對照圖 1 與程式片段 1,很容易看出『檔頭部分』使用的是 exec 結構。『程式碼部分』與『資料部分』則直接存放二進位目的碼,不需定義特殊的資料結構。而在『程式碼重定位的部分』與『資料重定位的部分』,使用的是 relocation_info 的結構。最後,在『符號表』與『字串表』的部分,使用的是 nlist 的結構。圖 2 顯示了此種對照關係,讀者可以很容易的辨認各分段的資料結構。 圖 2. 目的檔 a.out 各區段所對應的資料結構 當 a.out 檔案被載入時,載入器首先會讀取檔頭部分,接著根據檔頭保留適當的大小的記憶體空間 (包含程式段、資料段、BSS段、堆疊段、堆積段等)。然後,載入器會讀取程式段與資料段的目的碼,放入對應的記憶體中。接著,利用重定位資訊修改對應的記憶體資料。最後,才把程式計數器設定為 exec.a_entry所對應的位址,開始執行該程式,圖 3 顯示了 a.out 檔被載入的過程。 圖 3. 目的檔 a.out 的載入過程 目的檔格式a.out 是一種比較簡單而直接的格式,但其缺點是檔案格式太過固定,因此無法支援較為進階的功能,像是動態連結與載入等。目前,UNIX/Linux 普遍都已改用 ELF 格式作為標準的目的檔格式,在 Linux 2.6 當中就支援了動態載入的功能。在下一節當中,我們將介紹較為先進的目的檔格式 - ELF。 目的檔格式 – ELF目的檔ELF 格式(Executable and Linking Format) 是 UNIX/Linux 系統中較先進的目的檔格式。這種格式是 AT&T 公司在設計第五代UNIX (UNIX System V) 時所發展出來的。因此,ELF格式的主要文件被放在規格書 -『System V Application Binary Interface』的第四章的 Object Files當中 ,該文件詳細的介紹了 UNIX System V 中二進位檔案格式的存放方式。並且在第五章的 Program Loading and Dynamic Linking 當中,說明了動態連結與載入的設計方法。 雖然該規格書當中並沒有介紹與機器結構相關的部分,但由於各家CPU廠商都會自行撰寫與處理器有關的規格書,以補充該文件的不足之處,因此,若要查看與處理器相關的部分,可以查看各個廠商的補充文件 。 ELF可用來記錄目的檔 (object file)、執行檔 (executable file)、動態連結檔 (share object)、與核心傾印 (core dump) 檔等格式,並且支援較先進的動態連結與載入等功能。因此,ELF 格式在 UNIX/Linux 的設計上具有相當關鍵性的地位。 為了支援連結與執行等兩種時期的不同用途,ELF 格式可以分為兩種不同觀點,第一種是連結時期觀點 (Linking View),第二種是執行時期觀點 (Execution View)。圖 4 顯示了這兩種不同觀點的結構圖。在連結時期,是以分段 (Section) 為主的結構,如圖 4 (a) 所示,但在執行時期,則是以分區 (Segment) 為主的結構,如圖 4 (b) 所示。其中,一個區通常是數個分段的組合體,像是與程式有關的段落,包含程式段、程式重定位等,在執行時期會被組合為一個分區。 圖 4. 目的檔 ELF 的兩種不同觀點 因此,ELF 檔案有兩個不同用途的表頭,第一個是程式表頭 (Program Header Table),這個表頭記載了分區資訊,因此也可稱為分區表頭 (Segment Header Table)。程式表頭是執行時期的主要結構。而第二個表頭是分段表頭 (Section Header Table),記載了的分段資訊,是連結時期的主要結構。 程式片段 2 顯示了ELF 的檔頭結構 Elf32_Ehdr,其中的 e_phoff 指向程式表頭,而 e_shoff 指向分段表頭,透過這兩個欄位,我們可以取得兩種表頭資訊。 程式片段 2 : 目的檔 ELF 的檔頭結構 (Elf32_Ehdr)
在連結時期,連結器會以 ELF 的分段結構為主,利用分段表頭讀出各個分段。ELF 檔可支援任意數目的分段 (當然有上限,必須可以用16 位元整數表達)。而且,每個分段可以具有不同的結構,常見的分段有程式段 (.text) , 資料段 (.data), 唯讀資料段 (.rodata) , 未設定變數段 (.bss), 字串表 (.strtab), 符號表 (.symtab)等。但是,ELF為了支援較先進的連結載入方式,還包含了許多其他類型的段落,像是動態連結相關的區段等,表格 1 顯示了 ELF 中的常見分段名稱與其用途。 表格 1. 目的檔ELF 中的常見分段列表
由於 ELF 的分段眾多,我們將不詳細介紹每的段落的資料結構,只針對較重要或常見的資料結構進行說明。圖 5 顯示了ELF 檔案的分段與對應的資料結構,其中,檔頭結構是 Elf32_Ehdr、程式表頭結構是 Elf32_Phdr、分段表頭結構是 Elf32_Shdr。而在分段中,符號記錄 (Elf32_Sym) 、重定位記錄 (Elf32_Rel、Elf32_Rela)、與動態連結記錄 (Elf32_Dyn),是較重要的結構。 圖 5. 目的檔ELF的資料結構 分段表頭記錄了各分段 (Section) 的基本資訊,包含分段起始位址等,因此可以透過分段表頭讀取各分段,圖 6 顯示了如何透過分段表頭讀取分段的方法。程式片段 3 則顯示了分段表頭的結構定義程式。 圖 6. 目的檔ELF的分段表頭 程式片段 3 : ELF 的分段表頭記錄
程式表頭指向各個分區 (Segment) ,包含分區的起始位址,因此可以透過程式表頭取得各分區的詳細內容, 圖 7. 目的檔ELF的程式表頭 程式片段 4. 目的檔 ELF 的程式表頭結構
在靜態連結的情況之下,ELF的連結器同樣會合併 .text, .data, .bss 等段落,也會利用修改記錄 Elf32_Rel 與 Elf32_Rela,進行合併後的修正動作。而且,不同類型的分段會被組合成分區,像是.text, .rodata, .hash, .dynsym, .dynstr, .plt, .rel.got 等分段會被併入到內文區 (Text Segment) 當中。而 .data, .dynamic, .got, .bss 等分段則會被併入到資料區 (Data Segemnt) 當中。 Elf32_Sym 儲存了符號記錄,包含名稱 (st_name)、值 (st_value)、大小 (st_size)、資訊 (st_info)、其他 (st_other)、分段代號 (st_shndx) 等,其中 st_info 欄位又可細分為兩個子欄位,前四個位元是 bind 欄,用來記錄符號的屬性,後四個位元是 type欄,用來記錄符號的類型。 程式片段 5. 目的檔ELF的符號記錄
Elf32_Rel 與 Elf32_Rela 是 ELF 檔的兩種重定位記錄,兩者均包含位址欄 (r_offset) 與資訊欄 (r_info),其中資訊欄又可分為兩個子欄位,前面的 byte 是符號代號,後面的 byte 記錄符號類型。另外,在 Elf32_Rela 中,多了一個外加的數值欄位 (r_addend),可用來儲存重定位的位移值。 程式片段 6. 目的檔ELF的重定位記錄
重定位記錄 Elf32_rel 的 r_info 欄中的 sym 子欄位,會儲存符號表的索引值,因此,程式可以透過 sym 子欄位取得符號記錄。然後,在符號記錄 Elf32_Sym 中的 st_name 欄位,會儲存字串表中的索引值,因此,可以透過 st_name 取得符號的名稱。透過 sym 與 st_name 欄位,可將重定位表、符號表與字串表關連起來,圖 8 顯示了這三個表格的關連狀況圖。 圖 8. 目的檔ELF中的重定位表、符號表與字串表的關連性 雖然分段結構主要式為了連結時使用的,但是,如果不考慮動態連結的情況,載入器也可以利用分段結構直接進行載入。只要載入 .text, .data, .data2, .bss等區段,然後利用 .rel.text, .rel.data, .rel.data2, .rela.text, .rela.data, .rela.data2 等分段進行修改的動作,就能載入 ELF目的檔了。 但是,為了支援動態連結與載入 的技術,ELF 當中多了許多相關的分段,包含解譯段 (.interp)、動態連結段 (.dynamic)、全域位移表 (Global Offset Table : .got)、程序連結表 (Porcedure Linkage Table : .plt) 等,另外還有動態連結專用的字串表 (.dynstr) 、符號表 (.dynsym)、映射表 (.hash)、全域修改記錄 (rel.got) 等作為輔助。 執行ELF載入動作時,使用的是以區塊為主的執行時期觀點,常見的區塊包含程式表頭 (PHDR)、解譯區塊 (INTERP)、載入區塊(LOAD)、動態區塊 (DYNAMIC)、註解區塊 (NOTE)、共用函式庫區塊 (SHLIB) 等。其中,載入區塊通常有兩個以上,如此才能容納程式區塊 (TEXT) 與資料區塊 (DATA) 等不同屬性的區域。 表格 2 目的檔ELF 的常見區塊列表
在 Linux 當中,一般目的檔的附檔名是 .o (Object File),而動態連結函式庫的附檔名是 .so (Shared Object)。當程式被編譯為 .so 檔時,ELF目的檔中才會有INTERP 區塊,這個區塊中記錄了動態載入器的相關資訊,ELF載入器可透過這些資訊找到動態載入器 (ELF 文件中稱為Program Interpreter,但若稱為 Dynamic Loader 或許更恰當)。然後,當目的檔載入完成後,就可以開始執行,一但需要使用到動態函數時,才能利用動態載入器將動態函式庫載入。 通常,載入的動作是由作業系統的核心 (Kernel) 所負責的,載入器是作業系統的一部分。例如,Linux 作業系統的核心就會負責載入 ELF 格式的檔案,ELF 檔案的載入過程大致如下所示: 1. Kernel 將 ELF 檔案中的所有 PT_LOAD 型態的區塊載入到記憶體,這些區塊包含程式區塊與資料區塊。 ELF 檔案的載入過程,會因 CPU 的結構不同而有差異,因此,在 ELF 文件中這些與 CPU 有關的主題都被分離出來,由各家 CPU 廠商自行撰寫。舉例而言,動態函數的呼叫就是一個與 CPU 有關的主題,不同的 CPU實作方法會有所不同。 程式片段 7. IA32 處理器中的靜態函數呼叫與動態函數呼叫方式
程式片段 8. IA32 處理器中的動態連結函數區 (Stub) 的程式
程式片段 9 顯示了 ELF目的檔的動態連結記錄 Elf32_Dyn,這些記錄會被儲存在一個名為 _DYNAMIC[] 的陣列中,以便讓動態連結器使用。 程式片段 9. 目的檔ELF的動態連結 (重定位) 記錄
有關 ELF 目的檔的進一步資訊,有興趣的讀者可以參考規格書 System V Application Binary Interface 中的第四章與第五章 。 說明與參考文獻
|
Linux 的目的檔格式 - a.out 與 ELF 之設計原理
page revision: 18, last edited: 11 Oct 2010 06:43
Post preview:
Close preview