扒一扒ELF檔案

嵌入式與Linux那些事發表於2021-01-30

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

1. 連結舉例

  在介紹ELF檔案之前,我們先看下,一個.c程式是如何變成可執行目標檔案的。下面舉個例子。

  該程式由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檔案叫做可執行目標檔案。那這兩個目標檔案有什麼樣的區別呢?

2. ELF檔案型別

2.1 可重定位目標檔案(.o檔案)

  包含二進位制程式碼和資料,其形式可以和其他目標檔案進行合併,建立一個可執行目標檔案。例如lib*.o檔案。

2.2 可執行目標檔案(a.out檔案)

  包含二進位制程式碼和資料,可直接被載入器載入執行。例如編譯好的可執行檔案a.out。

2.3 共享物件檔案(.so檔案)

  用於和其他共享目標檔案或者可重定位檔案一起生成ELF目標檔案或者和執行檔案一起建立程式映像,例如lib*.so檔案。

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

本文參考

《深入理解計算機系統》

相關文章