1. 程式人生 > >ELF文件結構描述

ELF文件結構描述

方式 一起 文件結構 多個 包含 center global 個數字 lob


ELF目標文件格式最前部ELF文件頭(ELF Header),它包含了描述了整個文件的基本屬性,比如ELF文件版本、目標機器型號、程序入口地址等。其中ELF文件與段有關的重要結構就是段表(Section Header Table)

文件頭

我們可以使用readelf命令來詳細查看elf文件,代碼如清單3-2所示:
技術分享圖片
從上面輸出的結構可以看到:ELF文件頭定義了ELF魔數、文件機器字節長度、數據存儲方式、版本、運行平臺等。

ELF文件頭結構及相關常數被定義在“/usr/include/elf.h”,因為ELF文件在各種平臺下都通用,ELF文件有32位版本和64位版本的ELF文件的文件頭內容是一樣的,只不過有些成員的大小不一樣。它的文件圖也有兩種版本:分別叫“Elf32_Ehdr”

“Elf64_Ehdr”

typedef struct {
    unsigned char e_ident[16];  
    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;
    Elf32_Half e_phentsize;
    Elf32_Half e_phnum;
    Elf32_Half e_shentsize;
    Elf32_Half e_shnum;
    Elf32_Half e_shstrndx;
}Elf32_Ehdr;

技術分享圖片

技術分享圖片

段表

段表就是保存ELF文件中各種各樣段的基本屬性的結構。段表是ELF除了文件以外的最重要結構體,它描述了ELF的各個段的信息,ELF文件的段結構就是由段表決定的。編譯器、鏈接器和裝載器都是依靠段表來定位和訪問各個段的屬性的。段表在ELF文件中的位置由ELF文件頭的“e_shoff”成員決定的,比如SimpleSection.o中,段表位於偏移0x118。
技術分享圖片

重定位表

我們註意到:SimpleSection.o中有一個叫做".rel.text"的段,它的類型是(sh_type)為“SHT_REL”,也就是說它是一個重定位表(Relocation Table)。正如我們開始所說的,鏈接器在處理目標文件時,須要對目標文件中某些部位進行重定位,即代碼段和數據段中哪些對絕對地址的引用的位置。這些重定位的信息都記錄在ELF文件的重定位表裏面,對於每個須要重定位代碼段或數據段,都會有一個相應的重定位表。

字符串表

ELF文件中用到了許多的字符串,比如段名,變量名等。因為字符串的長度往往是不定的,所以用固定的結構來表示它比較困難。一種常見的做法是把字符串集中起來存放到一個表,然後使用字符串在表中的偏移來引用字符串。
通常用這種方式,在ELF文件中引用字符串只需給一個數字下標即可,不用考慮字符串的長度問題。一般字符串標在ELF文件中國也以段的方式保存,常見的段名為“.strtab”或“.shstrtab”。這兩個字符串分別表示為字符串表和段表字符串表。
只有分析ELF文件頭,就可以得到段表和段表字符串表的位置,從而解析整個ELF文件。

鏈接的接口-符號

鏈接的過程的本質就是要把多個不同目標文件之間相互“粘”到一起。或者說像玩具積木一樣,可以拼裝成一個整體。為了使不同目標文件之間能夠相互黏合,這些目標文件之間必須有固定的規則才行,就像積木模塊必須有凹凸部分才能相互黏合。在鏈接中,目標文件之間相互拼合實際上是目標文件之間對地址的引用,即對函數和變量的地址的引用。
比如目標文件B要用到目標文件中的函數“foo”,那麽我們就成目標文件A定義(Define)了函數“foo”,稱目標文件B引用(Reference)了目標文件A中的函數“foo”。這兩個概念也同樣適用於變量。每個函數或變量都有自己獨特的名字,才能避免鏈接過程中不同變量和函數之間的混淆。在鏈接中,我們將函數和變量統稱為符號(Symbol),函數名或變量名就是符號名(Symbol Name)。
我們可以將符號看作是鏈接中的粘合劑,整個鏈接過程正是基於符號才能夠完成。鏈接過程中很關鍵的一部分是符號的管理,每一個目標文件都會有一個相應的符號表(Symbol Table),這個表裏記錄了目標文件所用到的所有符號。每個定義的符號有一個對應的值,叫做符號值(Symbol Value),對於變量和函數來說,符號值就是它們的地址,除了函數和變量之外,還存在著其他幾種不常用的符號。我們將符號表中的所有符號進行分類,它們有可能是下面這些類型中的幾種:

  • 定義在本目文件中的全局符號,可以被其他目標文件引用,比如SimpleSection.o裏面的“func1”、“main”和“global_init_val”。
  • 在本目標文件中引用的全局符號,卻沒有定義在本目標文件,這一般叫做外部符號(External Symbol),也就是我們前面所講的符號引用。比如SimpleSection.o裏面的“printf”
  • 段名,這種符號往往是由編譯器產生,它的值就是該段的起始地址。
  • 局部符號,這類符號只在編譯單元內部可見。
  • 行號信息。
    對於我們來說,最值得關註的是全局符號,即上面的第一類和第二類。

ELF符號表結構

ELF文件中的符號表往往是文件中的一個段,段名一般叫做“.symtab”。符號表的結構很簡單,它是一個Elf32_Sym結構(32位ELF文件)的數組,每個Elf32_Sym結構對應一個符號。這個數組的第一個元素,也就是下標0的元素為無效的“未定義”符號。Elf32_Sym的結構對應如下:

typedef struct {
    Elf32_Word st_name;
    Elf32_Addr st_value;
    Elf32_Word st_size;
    unsigned char st_info;
    unsigned char st_other;
    Elf32_Half st_shndx;
} Elf32_Sym;
st_name 符號名。這個成員包含了該符號名在字符串表中的下標
st_value 符號對應的值。這個值跟符號有關,可能是一個絕對值
st_size 符號大小
st_info 符號類型和綁定信息
st_other 該成員目前為0,沒用
st_shndx 符號所在段

特殊符號

當我們使用ld作為鏈接器產生可執行文件,它會為我們定義很多特殊符號。這些符號並沒有在你的程序中定義,但是你可以直接聲明並引用它,我們稱之為特殊符號。其實這些符號是被定義在鏈接器腳本中的,我們無須定義它們,但可以聲明它們並且使用它們。鏈接器在程序最終連接成可執行文件將其解析成正確的值,註意,只有使用ld鏈接生成最終可執行文件的時候這些符號才會存在。
幾個很具有代表性的特殊符號如下:

  • __executable_start,該符號為程序的起始地址
  • __etext或_etext或etext,該符號為代碼段結束地址,即代碼段最末尾的地址
  • _edata或edata,該符號為數據段結束地址,即數據段的最末尾地址。
  • _end或end,該符號為程序的結束地址。
  • 以上地址都為程序被裝載的虛擬地址。
    我們可以在程序中直接使用這些符號。

符號修飾和函數簽名

在早期,編譯器編譯源代碼產生目標文件時,符號名與相應的變量和函數名字一樣的。比如在一個匯編源代碼中包含了一個函數foo,那麽匯編器將它編譯成目標文件後,foo在目標文件中所對應的符號名也是foo。後來的UNIX平臺和C語言發明時,已經存在了相當多的使用匯編編寫的庫和目標文件。這樣就產生了一個問題,那就是如果一個c程序要使用這些庫的話,C語言不可以使用這些庫中定義的函數和變量的名字作為符號名,否則將會跟現有的目標文件沖突。比如有個用匯編語言編寫的庫定義了一個函數叫做main,那麽我們在C語言裏面就不可以定義一個main函數或變量了。同樣的道理,如果一個C語言的目標文件要用到一個使用Fortran語言編寫的目標文件,我們也必須防止它們的名稱沖突。
為了防止類似的符號名沖突,UNIX的C語言就規定,C語言源代碼文件中的所有全局變量和函數經過編譯後,相對應的符號名加上“”。而Fortran語言的源代碼經過編譯以後,所有符號名前加上“”,後面也加上“_”
這種方式雖然能夠減少沖突的概率,但還是有可能造成沖突。於是C++開始考慮到這個問題,增加了namespace來解決多模塊的符號沖突問題。

ELF文件結構描述