目的檔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)
typedef struct {
unsigned char e_ident[EI_NIDENT]; // ELF 辨識代號區
Elf32_Half e_type; // 檔案類型代號
Elf32_Half e_machine; // 機器平台代號
Elf32_Word e_version; // 版本資訊
Elf32_Addr e_entry; // 程式的起始位址
Elf32_Off e_phoff; // 程式表頭的位址
Elf32_Off e_shoff; // 分段表頭的位址
Elf32_Word e_flags; // 與處理器有關的旗標值
Elf32_Half e_ehsize; // ELF檔頭的長度
Elf32_Half e_phentsize; // 程式表頭的記錄長度
Elf32_Half e_phnum; // 程式表頭的記錄個數
Elf32_Half e_shentsize; // 分段表頭的記錄長度
Elf32_Half e_shnum; // 分段表頭的記錄個數
Elf32_Half e_shstrndx; // 分段字串表 .shstrtab 的分段代號
} Elf32_Ehdr;
在連結時期,連結器會以 ELF 的分段結構為主,利用分段表頭讀出各個分段。ELF 檔可支援任意數目的分段 (當然有上限,必須可以用16 位元整數表達)。而且,每個分段可以具有不同的結構,常見的分段有程式段 (.text) , 資料段 (.data), 唯讀資料段 (.rodata) , 未設定變數段 (.bss), 字串表 (.strtab), 符號表 (.symtab)等。但是,ELF為了支援較先進的連結載入方式,還包含了許多其他類型的段落,像是動態連結相關的區段等,表格 1 顯示了 ELF 中的常見分段名稱與其用途。
表格 1. 目的檔ELF 中的常見分段列表
分段名稱 | 說明 |
.text | 程式段 |
.data | |
.data1 | 資料段 |
.bss | 未設初值的全域變數 |
.rodata | |
.rodata1 | 唯讀資料段 |
.dynamic | 動態連結資訊 |
.dynstr | 動態連結用字串表 |
.dynsym | 動態連結用符號表 |
.got | 動態連結用的全域位移表 (Global Offset Table) |
.plt | 動態連結用的程序連結表 (Procedure Linkage Table) |
.interp | 記錄程式解譯器的路徑 (program interpreter file) |
.ctors | 物件導向中的建構函數 (constructor) (C++可用) |
.dtors | 物件導向中的解構函數 (destructor) (C++可用) |
.hash | 雜湊表 |
.init | 在主程式執行前會執行此段落 |
.fini | 在主程式執行後會執行此段落 |
.rel<name> | |
.rela<name> | 重定位資訊,例如: rel.text 是程式段的重定位資訊,rel.data 則是資料段的重定位資訊。 |
.shstrtab | 儲存分段 (Section) 名稱 |
.strtab | 字串表 |
.symtab | 符號表 |
.debug | 除錯資訊 (保留給未來用) |
.line | 除錯時的行號資訊 |
.comment | 版本控制訊息 |
.note | 附註資訊 |
由於 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 的分段表頭記錄
typedef struct {
Elf32_Word sh_name; // 分段名稱代號
Elf32_Word sh_type; // 分段類型
Elf32_Word sh_flags; // 分段旗標
Elf32_Addr sh_addr; // 分段位址 (在記憶體中的位址)
Elf32_Off sh_offset; // 分段位移 (在目的檔中的位址)
Elf32_Word sh_size; // 分段大小
Elf32_Word sh_link; // 連結指標 (依據分段類型而定)
Elf32_Word sh_info; // 分段資訊
Elf32_Word sh_addralign; // 對齊資訊
Elf32_Word sh_entsize; // 分段中的結構大小 (分段包含子結構時使用)
} Elf32_Shdr;
程式表頭指向各個分區 (Segment) ,包含分區的起始位址,因此可以透過程式表頭取得各分區的詳細內容,
圖 7 顯示了如何透過程式表頭取得各分區的方法。 程式片段 4 則顯示了程式表頭的結構定義程式。
圖 7. 目的檔ELF的程式表頭
程式片段 4. 目的檔 ELF 的程式表頭結構
typedef struct {
Elf32_Word p_type; // 分區類型
Elf32_Off p_offset; // 分區位址 (在目的檔中)
Elf32_Addr p_vaddr; // 分區的虛擬記憶體位址
Elf32_Addr p_paddr; // 分區的實體記憶體位址
Elf32_Word p_filesz; // 分區在檔案中的大小
Elf32_Word p_memsz; // 分區在記憶體中的大小
Elf32_Word p_flags; // 分區旗標
Elf32_Word p_align; // 分區的對齊資訊
} Elf32_Phdr;
在靜態連結的情況之下,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的符號記錄
typedef struct
{
Elf32_Word st_name; // 符號名稱的代號
Elf32_Addr st_value; // 符號的值,通常是位址
Elf32_Word st_size; // 符號的大小,以 byte為單位
unsigned char st_info; // 細分為bind與type兩欄位
unsigned char st_other; // 目前為 0,保留未來使用
Elf32_Half st_shndx; // 符號所在的分段 (Section) 代號
} Elf32_Sym;
#define ELF32_ST_BIND(i) ((i) >> 4) // 取出 st_info 中的bind欄位
#define ELF32_ST_TYPE(i) ((i)&0xf) // 取出 st_info 中的type欄位
#define ELF32_ST_INFO(b,t) (((b)<<4)+((t)&0xf)) // 將 bind 與 type 組成 info
Elf32_Rel 與 Elf32_Rela 是 ELF 檔的兩種重定位記錄,兩者均包含位址欄 (r_offset) 與資訊欄 (r_info),其中資訊欄又可分為兩個子欄位,前面的 byte 是符號代號,後面的 byte 記錄符號類型。另外,在 Elf32_Rela 中,多了一個外加的數值欄位 (r_addend),可用來儲存重定位的位移值。
程式片段 6. 目的檔ELF的重定位記錄
typedef struct
{
Elf32_Addr r_offset; // 符號的位址
Elf32_Word r_info; // r_info可分為sym與type兩欄
} Elf32_Rel;
typedef struct
{
Elf32_Addr r_offset; // 符號的位址
Elf32_Word r_info; // r_info可分為sym與type兩欄
Elf32_Sword r_addend; // 外加的數值
} Elf32_Rela;
#define ELF32_R_SYM(i) ((i)>>8)
#define ELF32_R_TYPE(i) ((unsigned char) (i))
#define ELF32_R_INFO(s,t) (((s)<<8) + (unsigned char) (t))
重定位記錄 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 的常見區塊列表
Segment (區塊型態) | Sections (分段) | 說明 |
PT_PHDR | Program Header | 表頭段,用來計算基底位址 (base address) |
PT_INTERP | .interp | 動態載入區段。 |
PT_LOAD | .interp .note .hash .dynsym .dynstr .rel.dyn .rel.plt .init .plt .text .fini .rodata … | 載入器將此區塊載入程式段。 |
PT_LOAD | .data .dynamic .ctors .dtors .jcr .got .bss | 載入器將此區塊載入資料段。 |
PT_DYNAMIC | .dynamic | 由動態載入器處理 |
在 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 型態的區塊載入到記憶體,這些區塊包含程式區塊與資料區塊。
2. Kernel 將載入的區塊映射到該行程的虛擬位址空間中 (例如使用 linux 的 mmap 系統呼叫)。
3. Kernel 找到 PT_INTERP 型態的區塊,並根據區塊內的資訊找到動態連結器 (Dynamic Linker ) 的ELF檔 。
4. Kernel 將動態連結器載入到記憶體,並將其映射到該行程的虛擬位址空間中,然後啟動『動態連結器』。
5. 目的程式開始執行,在呼叫動態函數時,『動態連結器』根據需要,決定出正確的連結順序,然後對該程式與動態函數進行重定位的動作,再將控制權轉移到動態函數中。
ELF 檔案的載入過程,會因 CPU 的結構不同而有差異,因此,在 ELF 文件中這些與 CPU 有關的主題都被分離出來,由各家 CPU 廠商自行撰寫。舉例而言,動態函數的呼叫就是一個與 CPU 有關的主題,不同的 CPU實作方法會有所不同。
程式片段 7. IA32 處理器中的靜態函數呼叫與動態函數呼叫方式
C語言程式 靜態函數呼叫 動態函數呼叫
extern int var; pushl var movl var@GOT(%ebx)
extern int func(int); call func pushl (%eax)
call func@PLT
int call_func(void) {
return func(var);
}
程式片段 8. IA32 處理器中的動態連結函數區 (Stub) 的程式
.PLT0: pushl 4(%ebx)
Jmp *8(%ebx)
nop
nop
.PLT1: jmp *name1@GOT(%ebx)
pushl $offset1
jmp .PLT0@PC
.PLT2 jmp *name2@GOT(%ebx)
pushl $offset2
jmp .PLT0@PC
程式片段 9 顯示了 ELF目的檔的動態連結記錄 Elf32_Dyn,這些記錄會被儲存在一個名為 _DYNAMIC[] 的陣列中,以便讓動態連結器使用。
程式片段 9. 目的檔ELF的動態連結 (重定位) 記錄
typedef struct {
Elf32_Sword d_tag; // 標記
union {
Elf32_Word d_val; // 值 (用途很多樣)
Elf32_Addr d_ptr; // 指標 (程式的虛擬位址)
} d_un;
} Elf32_Dyn;
extern Elf32_Dyn _DYNAMIC[]; // 動態連結陣列
有關 ELF 目的檔的進一步資訊,有興趣的讀者可以參考規格書 System V Application Binary Interface 中的第四章與第五章 。