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
[cpp] view plain copy 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);
}
然後,産生目标檔案:
[cpp] view plain copy [[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 工具來檢視一下:
[cpp] view plain copy [[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 指出了段表當中用到的字元串表在段表中的下标。
檔案頭之後,緊跟着的是 程式頭,因為目标檔案沒有連結,是以沒有裝載資訊。我們這裡可以先不理會這個東西,以後專門再說他。
程式頭之後就是各個段的資料,我們用工具檢視一下:
[cpp] view plain copy [[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 的開始位址也由于記憶體對齊的要求,補了一個空位元組。