1. 程式人生 > >Linux下可執行檔案格式詳解

Linux下可執行檔案格式詳解

Linux下面,目標檔案、共享物件檔案、可執行檔案都是使用ELF檔案格式來儲存的。程式經過編譯之後會輸出目標檔案,然後經過連結可以產生可執行檔案或者共享物件檔案。Linux下面使用的ELF檔案和Windows作業系統使用的PE檔案都是從Unix系統的COFF檔案格式演化來的。 

我們先來了解一些基本的想法。

首先,最重要的思路是一個程式從人能讀懂的格式轉換為供作業系統執行的二進位制格式之後,程式碼和資料是分開存放的,之所以這樣設計有這麼幾個原因:

1、程式執行之後,程式碼和資料可以被對映到不同屬性的虛擬記憶體中。因為程式碼一般是隻讀的,而資料是可讀可寫的;

2、現代CPU有強大的快取體系。程式和程式碼分離可以提高程式的區域性性,增加快取命中的概率;

3、還有最重要的一個原因是當有多個程式副本在執行的時候,只讀部分可以只在記憶體中保留一份,這樣大大節省了記憶體。

在ELF的定義中,把他們分開存放的地方稱為一個 Section ,就是一個段。

一個ELF檔案中重要的段包括:

.text 段:儲存 只讀程式

.data 段:儲存 已經初始化的全域性變數和靜態變數

.bss 段:儲存 未初始化的全域性變數和靜態變數,因為這些變數的值為0,所以這個段在檔案當中不佔據空間

.rodata 段:儲存 只讀資料,比如字串常量

我們用一個例子來看一下ELF檔案的格式到底是什麼。首先,在Linux下編寫一個C程式:SimpleSection.c

int printf(const char *format, ... );

int global_init_var = 16;
int global_unint_var;

void func1 (int );

int main()
{
    static int static_var = -32;
    static int static_var_uninit;

    int a = 1;
    int b;

    func1(static_var + global_init_var + a + b);

    return a;
}

void func1 (int i)
{
    printf("%d\n", i);
}

然後,產生目標檔案:
[[email protected] Program]# gcc -c SimpleSection.c
[[email protected] Program]# file SimpleSection.o
SimpleSection.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

file命令的結果也告訴我們,這是一個32位ELF的檔案,型別是 relocatable ,就是可重定位。所以目標檔案又叫做可重定位檔案。

elf檔案的最開始是elf檔案頭資訊,32位有52個位元組組成。我們可以使用 readelf 工具來檢視一下:

[[email protected] Program]# readelf -h SimpleSection.o
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          224 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           40 (bytes)
  Number of section headers:         11
  Section header string table index: 8

Entry point address 指的是程式入口地址,如果是可執行檔案,這個欄位會有值;

他之前的欄位是一些說明欄位;

Start of program headers 指的是 程式頭表 的起始位置。程式頭表 是從裝載檢視的角度對elf的各個段進行的分類資訊;結構和段表相似;

Start of section headers 指出了elf除檔案頭以外的最重要的資訊:段表 的起始位置。段表包含了各個段的名稱、屬性、大小、位置等重要資訊。作業系統首先找到段表,然後根據段表的資訊去找到各個段。段表是一個類似陣列的結構,一個段的資訊是這個陣列的一個元素。

Size of this header 指的是標頭檔案大小,32位都是 52 個位元組,0x34個位元組。

Size of program headers 指的是每個 程式頭表 的大小。

Number of program headers 指的是 程式頭表 的數目。

Size of sections headers 指的是每個 段表 的大小;

Number of section headers 指的是 段表的數量;

Section header string table index 指出了段表當中用到的字串表在段表中的下標。

檔案頭之後,緊跟著的是 程式頭,因為目標檔案沒有連結,所以沒有裝載資訊。我們這裡可以先不理會這個東西,以後專門再說他。

程式頭之後就是各個段的資料,我們用工具檢視一下:

[[email protected] Program]# readelf -S SimpleSection.o
There are 11 section headers, starting at offset 0xe0:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 000020 00  AX  0   0  4
  [ 2] .rel.text         REL             00000000 0003f4 000010 08      9   1  4
  [ 3] .data             PROGBITS        00000000 000054 000008 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 00005c 000004 00  WA  0   0  4
  [ 5] .rodata           PROGBITS        00000000 00005c 000004 00   A  0   0  1
  [ 6] .comment          PROGBITS        00000000 000060 00002d 01  MS  0   0  1
  [ 7] .note.GNU-stack   PROGBITS        00000000 00008d 000000 00      0   0  1
  [ 8] .shstrtab         STRTAB          00000000 00008d 000051 00      0   0  1
  [ 9] .symtab           SYMTAB          00000000 000298 0000f0 10     10  10  4
  [10] .strtab           STRTAB          00000000 000388 00006b 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

各個欄位意思依次是:段序號、段名稱、段型別、段虛擬地址、偏移量、大小、ES、標誌、Lk、Inf、對齊。

沒有解釋的列可以先不考慮,我們先關注其他幾個列。

第0個段是為了讀取的時候下標不用減1。

緊跟著的就是程式碼段,偏移量為0x34,就是說在檔案頭結尾之後馬上就是程式碼段;

程式碼段之後,偏移量 0x54 的地方就是 資料段,佔8個位元組,就是程式中已經被賦值的一個全域性變數和一個靜態變數;

緊接著是.bss段,這裡只儲存了一個static變數,因為 未初始化的那個全域性變數被一種優化機制儲存到了 .common 段,這裡可以不做理會;

然後是隻讀資料段.rodata,這裡儲存的是 printf 裡面的 %d\n 這三個字元,外加結束符\0,總共4個位元組的空間

我們根據Size這一列來算一下這些段總共佔據的空間,(.bss由於不佔空間,不用算進來):

.text 0x20

.data 0x8

.rodata 0x4

.comment 0x2d

.shstrtab 0x51

.rel.text 0x10

.symtab 0xf0

.strtab 0x6b

這裡的每一個段都有一個段表元素來描述,總共11個。從標頭檔案得知,每個元素的大小為40位元組。也就是說段表總共佔了 0x1b8 個位元組的空間。而且段表的開始地址由於記憶體對齊需要,中間空了2個位元組。因為段表的開始地址是第224個位元組;

.rel.text 的開始地址也由於記憶體對齊的要求,補了一個空位元組。

在加上標頭檔案的 0x34 個位元組,總共加起來是   1028 位元組。

[[email protected] Program]# ls -al SimpleSection.o
-rw-r--r-- 1 root root 1028 Aug 21 16:09 SimpleSection.o

這個目標檔案的大小恰好是1028個位元組。