ELF檔案格式
ELF檔案(Executable Linkable Format)是一種檔案儲存格式。Linux下的目標檔案和可執行檔案都按照該格式進行儲存,有必要做個總結。
概要
本文主要記錄總結32位的Intel x86平臺下的ELF檔案結構。ELF檔案以Section的形式進行儲存。程式碼編譯後的指令放在程式碼段(Code Section),全域性變數和區域性靜態變數放到資料段(Data Section)。檔案以一個“檔案頭”開始,記錄了整個檔案的屬性資訊。
未連結的目標檔案結構
SimpleSection.c
int printf(const char* format, ...); int global_init_var = 84; int global_uniit_var; void func1(int i) { printf("%d\n", i); } int main(void) { static int static_var = 85; static int static_var2; int a = 1; int b; func1(static_var + static_var2 + a + b); return a; }
對於上面的一段c程式碼將其編譯但是不連結。 gcc -c -m32 SimpleSection.c
( -c表示只編譯不連結,-m32表示生成32位的彙編)得到SimpleSection.o。可以用objdump或readelf命令檢視目標檔案的結構和內容。
ELF檔案頭
可以用 readelf -h
檢視檔案頭資訊。執行 readelf -h SimpleSection.o
後:
root@DESKTOP-2A432QS:~/c# 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:832 (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:13 Section header string table index: 10
程式頭包含了很多重要的資訊,每個欄位的含義可參考 ofollow,noindex" target="_blank">ELF結構文件 。主要看下:
- Entry point address:程式的入口地址,這是沒有連結的目標檔案所以值是0x00
- Start of section headers:段表開始位置的首位元組
- Size of section headers:段表的長度(位元組為單位)
- Number of section headers:段表中項數,也就是有多少段
- Start of program headers:程式頭的其實位置(對於可執行檔案重要,現在為0)
- Size of program headers:程式頭大小(對於可執行檔案重要,現在為0)
- Number of program headers:程式頭中的項數,也就是多少Segment(和Section有區別,後面介紹)
- Size of this header:當前ELF檔案頭的大小,這裡是52位元組
段表及段(Section)
段表
ELF檔案由各種各樣的段組成,段表就是儲存各個段資訊的結構,以陣列形式存放。段表的起始位置,長度,項數分別由ELF檔案頭中的Start of section headers,Size of section headers,Number of section headers指出。使用 readelf -S SimpleSection.o
檢視SimpleSection.o的段表如下:
There are 13 section headers, starting at offset 0x340: Section Headers: [Nr] NameTypeAddrOffSizeES Flg Lk Inf Al [ 0]NULL00000000 000000 000000 00000 [ 1] .textPROGBITS00000000 000034 000062 00AX001 [ 2] .rel.textREL00000000 0002a8 000028 08I 1114 [ 3] .dataPROGBITS00000000 000098 000008 00WA004 [ 4] .bssNOBITS00000000 0000a0 000004 00WA004 [ 5] .rodataPROGBITS00000000 0000a0 000004 00A001 [ 6] .commentPROGBITS00000000 0000a4 000036 01MS001 [ 7] .note.GNU-stackPROGBITS00000000 0000da 000000 00001 [ 8] .eh_framePROGBITS00000000 0000dc 000064 00A004 [ 9] .rel.eh_frameREL00000000 0002d0 000010 08I 1184 [10] .shstrtabSTRTAB00000000 0002e0 00005f 00001 [11] .symtabSYMTAB00000000 000140 000100 1012114 [12] .strtabSTRTAB00000000 000240 000065 00001 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
總共有13個Section,重點關注.text, .data, .rodata, .symtab, .rel.text段。
程式碼段
.text段儲存程式碼編譯後的指令,可以用 objdump -s -d SimpleSection.o
檢視SimpleSection.o程式碼段的內容。
SimpleSection.o:file format elf32-i386 Contents of section .text: 0000 5589e583 ec0883ec 08ff7508 68000000U.........u.h... 0010 00e8fcff ffff83c4 1090c9c3 8d4c2404.............L$. 0020 83e4f0ff 71fc5589 e55183ec 14c745f0....q.U..Q....E. 0030 01000000 8b150400 0000a100 00000001................ 0040 c28b45f0 01c28b45 f401d083 ec0c50e8..E....E......P. 0050 fcffffff 83c4108b 45f08b4d fcc98d61........E..M...a 0060 fcc3.. ...省略 Disassembly of section .text: 00000000 <func1>: 0:55push%ebp 1:89 e5mov%esp,%ebp 3:83 ec 08sub$0x8,%esp 6:83 ec 08sub$0x8,%esp 9:ff 75 08pushl0x8(%ebp) c:68 00 00 00 00push$0x0 11:e8 fc ff ff ffcall12 <func1+0x12> 16:83 c4 10add$0x10,%esp 19:90nop 1a:c9leave 1b:c3ret 0000001c <main>: 1c:8d 4c 24 04lea0x4(%esp),%ecx 20:83 e4 f0and$0xfffffff0,%esp 23:ff 71 fcpushl-0x4(%ecx) 26:55push%ebp 27:89 e5mov%esp,%ebp 29:51push%ecx 2a:83 ec 14sub$0x14,%esp 2d:c7 45 f0 01 00 00 00movl$0x1,-0x10(%ebp) 34:8b 15 04 00 00 00mov0x4,%edx 3a:a1 00 00 00 00mov0x0,%eax 3f:01 c2add%eax,%edx 41:8b 45 f0mov-0x10(%ebp),%eax 44:01 c2add%eax,%edx 46:8b 45 f4mov-0xc(%ebp),%eax 49:01 d0add%edx,%eax 4b:83 ec 0csub$0xc,%esp 4e:50push%eax 4f:e8 fc ff ff ffcall50 <main+0x34> 54:83 c4 10add$0x10,%esp 57:8b 45 f0mov-0x10(%ebp),%eax 5a:8b 4d fcmov-0x4(%ebp),%ecx 5d:c9leave 5e:8d 61 fclea-0x4(%ecx),%esp 61:c3ret
可以看到.text段裡儲存的正是func1()和main()的指令。
資料段和只讀資料段
.data段儲存的是 已經初始化 了的全域性靜態變數和區域性靜態變數。前面SimpleSection.c中的global_init_varabal和static_var正是這樣的變數。使用 objdump -x -s -d SimpleSection.o
檢視:
Contents of section .data: 0000 54000000 55000000T...U... Contents of section .rodata: 0000 25640a00%d..
最左邊的0000是偏移,不用看,後面跟著的0x00000054和0x00000055正是global_init_varabal和static_var的初始值。
.rodata段存放的是隻讀資料,包括只讀變數(const修飾的變數和字串常量),這個例子中儲存了"%d\n"正是呼叫printf的時候使用的字元常量。
符號表段
符號表段一般叫做.symtab,以陣列結構儲存符號資訊(函式和變數),對於函式和變數符號值就是它們的地址。主要關注兩類符號:
- 定義在目標檔案中的全域性符號,可以被其他目標檔案引用,比如SimpleSction.o裡面的func1, main和global_init_var。
- 在本目標檔案中引用的全域性符號,卻沒有定義在本目標檔案,比如pritnf。
可以用 readelf -s SimpleSection.o
檢視SimpleSection.o的符號:
Symbol table '.symtab' contains 16 entries: Num:ValueSize TypeBindVisNdx Name 0: 000000000 NOTYPELOCALDEFAULTUND 1: 000000000 FILELOCALDEFAULTABS SimpleSection.c 2: 000000000 SECTION LOCALDEFAULT1 3: 000000000 SECTION LOCALDEFAULT3 4: 000000000 SECTION LOCALDEFAULT4 5: 000000000 SECTION LOCALDEFAULT5 6: 000000044 OBJECTLOCALDEFAULT3 static_var.1488 7: 000000004 OBJECTLOCALDEFAULT4 static_var2.1489 8: 000000000 SECTION LOCALDEFAULT7 9: 000000000 SECTION LOCALDEFAULT8 10: 000000000 SECTION LOCALDEFAULT6 11: 000000004 OBJECTGLOBAL DEFAULT3 global_init_var 12: 000000044 OBJECTGLOBAL DEFAULTCOM global_uniit_var 13: 0000000028 FUNCGLOBAL DEFAULT1 func1 14: 000000000 NOTYPEGLOBAL DEFAULTUND printf 15: 0000001c70 FUNCGLOBAL DEFAULT1 main
可以看到:
- func1和main的Ndx對應的值是1,表示在.text段(.text段在段表中的索引是1),型別是FUNC,value分別是0x00000000和0x0000001c,表明這兩個函式指令位元組碼的首位元組分別在.text段的0x00000000和0x0000001c偏移處。
- printf的Ndx是UND,表明這個符號沒有在SimpleSection.o中定義,僅僅是被引用。
- global_init_var和static_var.1488兩個符號的Ndx都是3,說明他們被定義在資料段,value分別是0x00000000和0x00000004,表示這個符號的位置在資料段的0x00000000和0x00000004偏移處,翻看上一節
Contents of section .data: 0000 54000000 55000000T...U...
資料段0x00000000和0x00000004偏移處儲存的正是global_init_var和static_var這兩個變數。
重定位表段
重定位表也是一個段,用於描述在重定位時連結器如何修改相應段裡的內容。對於.text段,對應的重定位表是.rel.text表。使用 objdump -r SimpleSection.o
檢視重定位表。
SimpleSection.o:file format elf32-i386 RELOCATION RECORDS FOR [.text]: OFFSETTYPEVALUE 0000000d R_386_32.rodata 00000012 R_386_PC32printf 00000036 R_386_32.data 0000003b R_386_32.bss 00000050 R_386_PC32func1
printf對應的那行的OFFSET為0x00000012,表明.text段的0x00000012偏移處需要修改。我們 objdump -s -d SimpleSection.o
檢視程式碼段的0x00000012偏移,發現是”fc ff ff ff“是call指令的運算元。
00000000 <func1>: 0:55push%ebp 1:89 e5mov%esp,%ebp 3:83 ec 08sub$0x8,%esp 6:83 ec 08sub$0x8,%esp 9:ff 75 08pushl0x8(%ebp) c:68 00 00 00 00push$0x0 11:e8 fc ff ff ffcall12 <func1+0x12> 16:83 c4 10add$0x10,%esp 19:90nop 1a:c9leave 1b:c3ret
也就是說,在沒有重定位前call指令的操作”fc ff ff ff“是無效的,需要在重定位過程中進行修正。func1那行也同理。
總結
ELF檔案結構可以用下面的圖表示:
可執行程式結構
和未連結的ELF檔案結構一樣,只不過引入了Segment的概念(注意和Section進行區分)。Segment本質上是從裝載的角度重新劃分了ELF的各個段。目標檔案連結成可執行檔案時,連結器會盡可能把相同許可權屬性的段(Section)分配到同一Segment。Segment結構的起始位置,項數,大小分別由ELF頭中的Size of program headers,Number of program headers, Size of this header欄位指定。
參考資料:
- 《程式設計師的自我修養》第3,6章
- ELF結構文件