Linux系統下的ELF文件分析

admin @ 2014-03-25 , reply:0

1 引言
    ELF(Executable and Linkable Format)即可執行連接文件格式,是Linux,SVR4和Solaris2.0默認的目標文件格式,目前標準介面委員會TIS已將ELF標準化為一種可移植的目標文件格式,運行於32-bit Intel體系微機上,可與多種操作系統兼容。分析elf文件有助於理解一些重要的系統概念,例如程序的編譯和鏈接,程序的載入和運行等

2 ELF文件格式
2.1 ELF文件的類型
ELF文件主要有三種類型:
(1)可重定位文件包含了代碼和數據.可與其它ELF文件建立一個可執行或共享的文件:
(2)可執行文件時可直接執行的程序:
(3)共享目標文件包括代碼和數據,可以在兩個地方鏈接。第一,連接器可以把它和其它可重定位文件和共享文件一起處理以建立另一個ELF文件;第二,動態鏈接器把它和一個可執行文件和其它共享文件結合在一起建立一個進程映像。

2.2 ELF文件的組織
    ELF文件參與程序的連接(建立一個程序)和程序的執行(運行一個程序),編譯器和鏈接器將其視為節頭表(section header table)描述的一些節(section)的集合,而載入器則將其視為程序頭表(program header table)描述的段(segment)的集合,通常一個段可以包含多個節。可重定位文件都包含一個節頭表,可執行文件都包含一個程序頭表。共享文件兩者都包含有。為此,ELF文件格式同時提供了兩種看待文件內容的方式,反映了不同行為的不同要求。下圖顯示了ELF文件的組織。
 
2.3 文件頭(Elf header)
    Elf頭在程序的開始部位,作為引路表描述整個ELF的文件結構,其信息大致分為四部分:一是系統相關信息,二是目標文件類型,三是載入相關信息,四是鏈接相關信息 其中系統相關信息包括elf文件魔數(標識elf文件),平台位數,數據編碼方式,elf頭部版本,硬體平台e_machine,目標文件版本e_version,處理器特定標誌e_ftags:這些信息的引入極大增強了elf文件的可移植性,使交叉編譯成為可能。目標文件類型用e_type的值表示,可重定位文件為1,可執行文件為2,共享文件為3;載入相關信息有:程序進入點e_entry.程序頭表偏移量e_phoff,elf頭部長度e_ehsize,程序頭表中一個條目的長度e_phentsize,程序頭表條目數目e_phnum;鏈接相關信息有:節頭表偏移量e_shoff,節頭表中一個條目的長度e_shentsize,節頭表條目個數e_shnum ,節頭表字元索引e shstmdx。可使用readelf -h filename來察看文件頭的內容。
文件頭的數據結構如下:
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;//目標文件類型
Elf32_Half e_machine;//硬體平台
Elf32_Word e_version;//elf頭部版本
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_shstrmdx;//節頭表字元索引
}Elf32_Ehdr;

2.4 程序頭表(program header table)
    程序頭表告訴系統如何建立一個進程映像.它是從載入執行的角度來看待elf文件.從它的角度看.elf文件被分成許多段,elf文件中的代碼、鏈接信息和註釋都以段的形式存放。每個段都在程序頭表中有一個表項描述,包含以下屬性:段的類型,段的駐留位置相對於文件開始處的偏移,段在內存中的首位元組地址,段的物理地址,段在文件映像中的位元組數.段在內存映像中的位元組數,段在內存和文件中的對齊標記。可用readelf -l filename察看程序頭表中的內容。程序頭表的結構如下:
typedef struct elf32_phdr{
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;

2.5節頭表(section header table)
    節頭表描述程序節,為編譯器和鏈接器服務。它把elf文件分成了許多節.每個節保存著用於不同目的的數據.這些數據可能被前面的程序頭重複使用,完成一次任務所需的信息往往被分散到不同的節里。由於節中數據的用途不同,節被分成不同的類型,每種類型的節都有自己組織數據的方式。每一個節在節頭表中都有一個表項描述該節的屬性,節的屬性包括小節名在字元表中的索引,類型,屬性,運行時的虛擬地址,文件偏移,以位元組為單位的大小,小節的對齊等信息,可使用readelf -S filename來察看節頭表的內容。節頭表的結構如下:
typedef struct{
Elf32_Word sh_name;//小節名在字元表中的索引
E1t32_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; //一些sections保存著一張固定大小入口的表。就像符號表
}Elf32_Shdr;

3 ELF的特性
3.1平台相關
    在ELF 文件頭中包含了足夠的平台相關信息,如數據編碼方式,平台位數,硬體平台e_machine等,這些平台相關信息可在編譯由編譯器決定。例如,與平台位數的相關的數據結構的定義在elf.h的頭文件中.在編譯預處理時確定:
#if ELF CLASS==ELFCLASS32
extern Elf32_Dyn_DYNAMIC[];
#define elfhdr elf32_hdr;
#define elf_phdr elf32_phdr;
#define elf_note elf32_note;
#else
extern Elf64_Dyn_DYNAMIC[];
#define elfhdr elf64_hdr;
#define elf_phdr elf64_phdr;
#define elf_note elf64_note;
#endif
    linux系統載入ELF可執行文件時,必須首先做一些簡單的一致性檢查.其代碼如下
if(memcmp(elf_ex.e_ident,ELFMAG,SELFMAG)!=0)
goto out; //檢查文件頭開始四個字元是否為ELF魔數'\0177ELF
if(elf_ex.e_type!=ET_EXEC&&elf_ex.e_type!=ET_DYN)
goto out;//檢查文件類型是否為可執行文件或共享目標文件
if(!elf_check_arch(&elf_ex))
goto out;//檢查硬體平台是否一致
    其中的elf_check_arch(x)在不同的硬體平台上有不同的定義,其由系統的硬體平台決定。這樣,在硬體平台相同的系統上,ELF可以不作修改的執行。因此,它可以支持不同平台上的交叉編譯(cross_compilation)和交叉鏈接(cross_linking)。

3.2 PIC
    ELF可以生成一種特殊的代碼——與位置無關的代碼(position-independent code,PIC)。用戶對gcc使用-fPIC指示GNU編譯系統生成PIC代碼。它是實現共享庫或共享可執行代碼的基礎.這種代碼的特殊性在於它可以載入到內存地址空間的任何地址執行.這也是載入器可以很方便的在進程中動態鏈接共享庫。
    PIC的實現運用了一個事實,就是代碼段中任何指令和數據段中的任何變數之間的距離都是一個與代碼段和數據段的絕對存儲器位置無關的常量。因此,編譯器在數據段開始的地方創建了一個表.叫做全局偏移量表(global offset table.GOT)。GOT包含每個被這個目標模塊引用的全局數據目標的表目。編譯器還為GOT中每個表目生成一個重定位記錄。在載入時,動態鏈接器會重定位GOT中的每個表目,使得它包含正確的絕對地址。PIC代碼在代碼中實現通過GOT間接的引用每個全局變數,這樣,代碼中本來簡單的數據引用就變得複雜,必須加入得到GOT適當表目內容的指令。對只讀數據的引用也根據同樣的道理,所以,加上 IC編譯成的代碼比一般的代碼開銷大。
    如果一個elf可執行文件需要調用定義在共享庫中的任何函數,那麼它就有自己的GOT和PLT(procedure linkage table,過程鏈接表).這兩個節之間的交互可以實現延遲綁定(lazy binging),這種方法將過程地址的綁定推遲到第一次調用該函數。為了實現延遲綁定,GOT的頭三條表目是特殊的:GOT[0]包含.dynamic段的地址,.dynamic段包含了動態鏈接器用來綁定過程地址的信息,比如符號的位置和重定位信息;GOT[1]包含動態鏈接器的標識;GOT[2]包含動態鏈接器的延遲綁定代碼的入口點。GOT的其他表目為本模塊要引用的一個全局變數或函數的地址。PLT是一個以16位元組(32位平台中)表目的數組形式出現的代碼序列。其中PLT[0]是一個特殊的表目,它跳轉到動態鏈接器中執行;每個定義在共享庫中並被本模塊調用的函數在PLT中都有一個表目,從PLT[1]開始.模塊對函數的調用會轉到相應PLT表目中執行,這些表目由三條指令構成。第一條指令是跳轉到相應的GOT存儲的地址值中.第二條指令把函數相應的ID壓入棧中,第三條指令跳轉到PLT[O]中調用動態鏈接器解析函數地址,並把函數真正地址存入相應的GOT表目中。被調用函數GOT相應表目中存儲的最初地址為相應PLT表目中第二條指令的地址值,函數第一次被調用后.GOT表目中的值就為函數的真正地址。因此,第一次調用函數時開銷比較大.但是其後的每次調用都只會花費一條指令和一個間接的存儲器引用。

3.3 強大的工具支持
    由於gnu由大量的工具支持elf文件個時. 隨著gnu工具的功能的擴展.程序員對ELF文件的運用也越來越靈活。例如,在C++中全局的構造函數和析構函數必須非常小心的處理碰到的語言規範問題。構造函數必須在main函數之前被調用。析構函數必須在main函數返回之後被調用。ELF文件格式中,定義了兩個特殊的節(section),.init和.fini,.init保存著可執行指令,它構成了進程的初始化代碼。當一個程序開始運行時,在main函數被調用之前(c語言稱為main),系統安排執行這個section的中的代碼。.fini保存著可執行指令,它構成了進程的終止代碼。當一個程序正常退出時.系統安排執行這個section的中的代碼。C++編譯器利用這個特性.構造正確的.init和.fini sections.並結合.ctors(該section保存著程序的全局的構造函數的指針數組)和.dtors(該section保存著程序的全局的析構函數的指針數組)兩個section,完成全局的構造函數和析構函數的處理。
    GCC還有許多擴展的特性.有些對ELF 特別的有用。其中一個就是_attribute_ 。使用_attribute_可以使一個函數放到_CTOR_LIST_或者_DTOR_LIST_里。_attribute_((constructor))促使函數在進入main之前會被自動調用。_attribute_((destructor))促使函數在main返回或者exit調用之後被自動調用。這種函數必須是不能帶參數的而且必須是static void類型的函數。在ELF下,這個特性在一般的可執行文件和共享庫中都能很好的工作。另外一個GCC的特性是attribute_(section("sectionname")),使用這個,能把一個函數或者是數據結構放到任何的section中。

4 結論
    elf文件格式是一種比較複雜的文件格式,但其應用廣泛。與linux下的其他可執行文件(a.out,cof)相比,它對節的定義和gnu工具鏈對它的支持使它十分靈活,它保存的足夠了系統相關信息使它能支持不同平台上的交叉編譯和交叉鏈接,可移植性很強.同時它在執行中支持動態鏈接共享庫。




[admin via 研發互助社區 ] Linux系統下的ELF文件分析已經有1161次圍觀

http://cocdig.com/docs/show-post-42189.html