扒一扒ELF文件

ELF文件(Executable Linkable Format)是一種文件存儲格式。Linux下的目標文件和可執行文件都按照該格式進行存儲,有必要作個總結。linux

1. 連接舉例

  在介紹ELF文件以前,咱們先看下,一個.c程序是如何變成可執行目標文件的。下面舉個例子。ubuntu

  該程序由main.c和sum.c兩個模塊組成。sum.c接收數組和數組長度兩個參數,最後將數組求和的結果返回。main.c調用sum函數,並傳遞一個兩元素的int數組array,將計算結果保存在val中。數組

//main.c
int sum(int *a, int n);

int array[2] = {1, 2};

int main(int argc, char** argv)
{
    int val = sum(array, 2);
    return val;
}
//sum.c
int sum(int *a, int n)
{
    int i, s = 0;

    for (i = 0; i < n; i++) {
        s += a[i];
    }
    return s;
}

  讓咱們來看看若是咱們使用GCC編譯兩個模塊會發生什麼?數據結構

  main.c和sum.c將分別經過翻譯器將源文件處理爲可重定位的目標文件main.o和sum.o。翻譯器處理的過程包括了預處理(ccp)、編譯(ccl)、彙編(as)三個過程。最後,連接器(ld)將可重定位的目標文件main.o和sum.o以及一些必要的系統文件組合起來,建立一個可執行目標文件prog。具體過程以下圖所示。函數

連接過程

  由上面的過程,咱們能夠看出在通過彙編器後會輸出一個.o文件,這個叫作可重定位的目標文件。將main.o和sum.o輸入連接器後,連接器輸出的prog文件叫作可執行目標文件。那這兩個目標文件有什麼樣的區別呢?ui

2. ELF文件類型

2.1 可重定位目標文件(.o文件)

  包含二進制代碼和數據,其形式能夠和其餘目標文件進行合併,建立一個可執行目標文件。例如lib*.o文件。this

2.2 可執行目標文件(a.out文件)

  包含二進制代碼和數據,可直接被加載器加載執行。例如編譯好的可執行文件a.out。翻譯

2.3 共享對象文件(.so文件)

  用於和其餘共享目標文件或者可重定位文件一塊兒生成ELF目標文件或者和執行文件一塊兒建立進程映像,例如lib*.so文件。debug

3. ELF文件做用

  ELF文件參與程序的鏈接(創建一個程序)和程序的執行(運行一個程序),因此能夠從不一樣的角度來看待ELF格式的文件:調試

  1.若是用於編譯和連接(可重定位文件),則編譯器和連接器將把ELF文件看做是節頭表描述的節的集合,程序頭表可選。

  2.若是用於加載執行(可執行文件),則加載器則將把ELF文件看做是程序頭表描述的段的集合,一個段可能包含多個節,節頭表可選。

4. ELF文件格式

4.1 從編譯和連接角度看ELF文件(可重定位目標文件)

從編譯和連接角度看ELF文件

ELF頭

  每一個ELF文件都必須存在一個ELF_Header,這裏存放了不少重要的信息用來描述整個文件的組織,如: 版本信息,入口信息,偏移信息等。程序執行也必須依靠其提供的信息。

段頭表

  段頭表。存放的是全部不一樣段將在內存中的位置

.text section

  代碼段。存放已編譯程序的機器代碼,通常是隻讀的。

.rodata section

  只讀數據段。此段的數據不可修改,存放常量。好比,printf中的格式化語句。

.data section

  數據段。存放已初始化的全局變量、常量。

.bss section

  bss段。未初始化全局變量,僅是佔位符,不佔據任何實際磁盤空間。目標文件格式區分初始化和非初始化是爲了空間效率.

從編譯和連接角度看ELF文件

.symtab section

  符號表,它存放在程序中定義和引用的函數和全局變量的信息。

.rel.txt section

  .text節的重定位信息,用於從新修改代碼段的指令中的地址信息。

.rel.data section

  .data節的重定位信息,用於對被模塊使用或定義的全局變量進行重定位的信息。

.debug section

  調試用的符號表。

.strtab section

  包含 symtab和 debug節中符號及節名。

節頭部表

  每一個節的節名、偏移和大小。

  如下是32位系統對應的節頭表數據結構,說明了每一個節的節名、在文件中的偏移、大小、訪問屬性、對齊方式等。

typedef struct {
    Elf32_Word sh_name;   //節名字符串在.strtab節(字符串表)中的偏移
    Elf32_Word sh_type;   //節類型:無效/代碼或數據/符號/字符串/...
    Elf32_Word sh_flags;  //節標誌:該節在虛擬空間中的訪問屬性
    Elf32_Addr sh_addr;   //虛擬地址:若可被加載,則對應虛擬地址
    Elf32_Off  sh_offset; //在文件中的偏移地址,對.bss節而言則無心義
    Elf32_Word sh_size;   //節在文件中所佔的長度
    Elf32_Word sh_link;   //sh_link和sh_info用於與連接相關的節(如 .rel.text節、.rel.data節、.symtab節等)
    Elf32_Word sh_info;
    Elf32_Word sh_addralign; //節的對齊要求
    Elf32_Word sh_entsize;   //節中每一個表項的長度,0表示無固定長度表項
} Elf32_Shdr;

  使用readelf命令命令查看節頭表內容

[ubuntu@localhost interpositioning]$ readelf -S main.o
There are 13 section headers, starting at offset 0x3f8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000071  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  000002d0
       0000000000000090  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  000000b1
       0000000000000049  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  000000b1
       000000000000000c  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  000000b1
       0000000000000019  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  000000ca
       0000000000000035  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000ff
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  00000100
       0000000000000058  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  00000360
       0000000000000030  0000000000000018   I      11     8     8
  [10] .shstrtab         STRTAB           0000000000000000  00000390
       0000000000000061  0000000000000000           0     0     1
  [11] .symtab           SYMTAB           0000000000000000  00000158
       0000000000000150  0000000000000018          12     9     8
  [12] .strtab           STRTAB           0000000000000000  000002a8
       0000000000000023  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

  可重定位目標文件中,每一個可裝入節的起始地址老是0。

  .bss節應占000000000000000c大小,但只有裝入內存時纔會分配。

4.2 從程序執行角度看ELF文件(可執行文件)

從程序執行角度看ELF文件

  與可重定位文件的不一樣

  1.ELF頭中字段 e_entry給出執行程序時第一條指令的地址,而在可重定位文件中,此字段爲0。

  2.多一個init節,用於定義init函數,該函數用來進行可執行目標文件開始執行時的初始化工做。

  3.少兩.rel節(無需重定位)。

  4.多一個程序頭表,也稱段頭表,是一個結構數組。

  使用readelf命令查看ELF頭的內容:

[ubuntu@localhost interpositioning]$readelf -h main.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1064 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           32 (bytes)         //程序頭表每項32B
  Number of program headers:         8                  //程序頭表共8項
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 10                //.strtab在節頭表中的索引

  裝入內存時,ELF頭、程序頭表、.init節、.rodata節會被裝入只讀代碼段。.data節和.bss節會被裝入讀寫數據段。

  段頭表可以描述可執行文件中的節與虛擬空間中的存儲段之間的映射關係。一個表項32B,說明虛擬地址空間中一個連續的片斷或一個特殊的節。如下是32位系統對應的段頭表數據結構:

typedef struct {
    Elf32_Word p_type;   //此數組元素描述的段的類型,或者如何解釋此數組元素的信息。
    Elf32_Off p_offset;  //此成員給出從文件頭到該段第一個字節的偏移
    Elf32_Addr p_vaddr;  //此成員給出段的第一個字節將被放到內存中的虛擬地址
    Elf32_Addr p_paddr;  //此成員僅用於與物理地址相關的系統中。System V忽略全部應用程序的物理地址信息。
    Elf32_Word p_filesz; //此成員給出段在文件映像中所佔的字節數。能夠爲0。
    Elf32_Word p_memsz;  //此成員給出段在內存映像中佔用的字節數。能夠爲0。
    Elf32_Word p_flags;  //此成員給出與段相關的標誌。
    Elf32_Word p_align;  //此成員給出段在文件中和內存中如何對齊。
} Elf32_phdr;

  使用readelf命令某可執行目標文件的程序頭表

[ubuntu@localhost interpositioning]$readelf -l main

Elf file type is EXEC (Executable file)
Entry point 0x400550
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000008ac 0x00000000000008ac  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x0000000000000240 0x0000000000000248  RW     200000
  DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x0000000000000780 0x0000000000400780 0x0000000000400780
                 0x0000000000000034 0x0000000000000034  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x00000000000001f0 0x00000000000001f0  R      1

  程序頭表信息有9個表項,其中兩個爲可裝入段(即Type=LOAD):

  第一可裝入段(第15,16行):第0x00000~0x0x8ab的長度爲0x8ac字節的ELF頭、程序頭表、.init、.text和.rodata節,映射到虛擬地址0x400000開始長度爲0x8ac字節的區域 ,按0x200000=2MB對齊,具備只讀/執行權限(Flg=RE),是隻讀代碼段。

  第二可裝入段(第17,18行):第0xe10~0x104f的長度爲0x240字節的.data節和磁盤中不佔存儲空間的.bss節,映射到虛擬地址0x600e10開始長度爲0x248字節的存儲區域,在0x248=584B存儲區中,前0x240=576B用.data節內容初始化,後面584-576=8B對應.bss節,初始化爲0 ,按0x200000=2MB對齊,具備可讀可寫權限(Flg=RW),是可讀寫數據段。

  由此看出.bss節在文件中不佔用磁盤空間,但在存儲器中須要給它分配相應大小的空間

5.總結

  1.連接處理涉及到三種目標文件格式:可重定位目標文件、可執行目標文件和共享目標文件。共享庫文件是一種特殊的可重定位目標。

  2.ELF目標文件格式能夠從編譯連接角度程序執行角度兩個角度看,前者是可重定位目標格式,後者是可執行目標格式。從編譯連接角度看,可重定位目標文件中包含ELF頭、各個節以及節頭表。可執行目標文件中包含ELF頭、程序頭表(段頭表)以及各類節組成的段。

  3.bss段在可執行目標文件中不會有它的空間,只有當可執行目標文件裝載運行時,纔會被分配內存(而且位於data段內存塊以後),而且初始化爲0

本文參考

《深刻理解計算機系統》

相關文章
相關標籤/搜索