linux核心之elf格式
圖 18.1. ELF檔案
2.2.1目標檔案
下面用readelf
工具讀出目標檔案max.o
的ELF Header和Section Header Table,然後我們逐段分析。
$ readelf -a max.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: 200 (bytes into file) //從檔案地址200(0xc8)開始, // 每個Section Header佔40位元組,共40*8=320位元組,到檔案地址0x207結束。 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: 8 Section header string table index: 5 2.2.2 section頭 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 00002a 00 AX 0 0 4 [ 2] .rel.text REL 00000000 0002b0 000010 08 6 1 4 [ 3] .data PROGBITS 00000000 000060 000038 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 000098 000000 00 WA 0 0 4 [ 5] .shstrtab STRTAB 00000000 000098 000030 00 0 0 1 [ 6] .symtab SYMTAB 00000000 000208 000080 10 7 7 4 [ 7] .strtab STRTAB 00000000 000288 000028 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) There are no program headers in this file.
注:從Section Header中讀出各Section的描述資訊,
其中.text
和.data
是我們在彙編程式中宣告的Section,而其它Section是彙編器自動新增的。
Addr
是這些段載入到記憶體中的地址(我們講過程式中的地址都是虛擬地址),載入地址要在連結時填寫,現在空缺,所以是全0。
Off
和Size
列指出了各Section的起始檔案地址和長度。比如.data
段從檔案地址0x60開始,一共0x38個位元組,回去翻一下程式,.data
段定義了14個4位元組的整數,一共是56個位元組,也就是0x38。根據以上資訊可以描繪出整個目標檔案的佈局。
表 18.1. 目標檔案的佈局
起始檔案地址 | Section或Header |
---|---|
0 | ELF Header |
0x34 | .text |
0x60 | .data |
0x98 | .bss (此段為空) |
0x98 | .shstrtab |
0xc8 | Section Header Table |
0x208 | .symtab |
0x288 | .strtab |
0x2b0 | .rel.text |
這個檔案不大,我們直接用hexdump
工具把目標檔案的位元組全部打印出來看。
》hexdump -C max.o
00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 01 00 03 00 01 00 00 00 00 00 00 00 00 00 00 00 |................|
00000020 c8 00 00 00 00 00 00 00 34 00 00 00 00 00 28 00 |........4.....(.|
00000030 08 00 05 00 bf 00 00 00 00 8b 04 bd 00 00 00 00 |................|
00000040 89 c3 83 f8 00 74 10 47 8b 04 bd 00 00 00 00 39 |.....t.G.......9|
00000050 d8 7e ef 89 c3 eb eb b8 01 00 00 00 cd 80 00 00 |.~..............|
00000060 03 00 00 00 43 00 00 00 22 00 00 00 de 00 00 00 |....C...".......|
00000070 2d 00 00 00 4b 00 00 00 36 00 00 00 22 00 00 00 |-...K...6..."...|
00000080 2c 00 00 00 21 00 00 00 16 00 00 00 0b 00 00 00 |,...!...........|
00000090 42 00 00 00 00 00 00 00 00 2e 73 79 6d 74 61 62 |B.........symtab|
000000a0 00 2e 73 74 72 74 61 62 00 2e 73 68 73 74 72 74 |..strtab..shstrt|
000000b0 61 62 00 2e 72 65 6c 2e 74 65 78 74 00 2e 64 61 |ab..rel.text..da|
000000c0 74 61 00 2e 62 73 73 00 00 00 00 00 00 00 00 00 |ta..bss.........|
000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
我們繼續分析readelf
輸出的最後一部分,是從.rel.text
和.symtab
這兩個Section中讀出的資訊。
... Relocation section '.rel.text' at offset 0x2b0 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000008 00000201 R_386_32 00000000 .data 00000017 00000201 R_386_32 00000000 .data There are no unwind sections in this file. Symbol table '.symtab' contains 8 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 SECTION LOCAL DEFAULT 1 2: 00000000 0 SECTION LOCAL DEFAULT 3 3: 00000000 0 SECTION LOCAL DEFAULT 4 4: 00000000 0 NOTYPE LOCAL DEFAULT 3 data_items 5: 0000000e 0 NOTYPE LOCAL DEFAULT 1 start_loop 6: 00000023 0 NOTYPE LOCAL DEFAULT 1 loop_exit 7: 00000000 0 NOTYPE GLOBAL DEFAULT 1 _start No version information found in this file. 注:.rel.text
告訴連結器指令中的哪些地方需要做重定位,在下一小節詳細討論。.symtab
是符號表。Ndx
列是每個符號所在的Section編號, 例如符號data_items
在第3個Section裡(也就是.data
段), 各Section的編號見Section Header Table。Value
列是每個符號所代表的地址,在目標檔案中,符號地址都是相對於該符號所在Section的相對地址, 比如data_items
位於.data
段的開頭,所以地址是0,_start
位於.text
段的開頭,所以地址也是0, 但是start_loop
和loop_exit
相對於.text
段的地址就不是0了。 從Bind
這一列可以看出_start
這個符號是GLOBAL
的,而其它符號是LOCAL
的,GLOBAL
符號是在彙編程式中用.globl
指示宣告過的符號。 2.2.3
現在剩下.text
段沒有分析,objdump
工具可以把程式中的機器指令反彙編(Disassemble),那麼反彙編的結果是否跟原來寫的彙編程式碼一模一樣呢?我們對比分析一下。
$ objdump -d max.o max.o: file format elf32-i386 Disassembly of section .text: 00000000 <_start>: 0: bf 00 00 00 00 mov $0x0,%edi 5: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax c: 89 c3 mov %eax,%ebx 0000000e <start_loop>: e: 83 f8 00 cmp $0x0,%eax 11: 74 10 je 23 <loop_exit> 13: 47 inc %edi 14: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax 1b: 39 d8 cmp %ebx,%eax 1d: 7e ef jle e <start_loop> 1f: 89 c3 mov %eax,%ebx 21: eb eb jmp e <start_loop> 00000023 <loop_exit>: 23: b8 01 00 00 00 mov $0x1,%eax 28: cd 80 int $0x80
2.3. 可執行檔案
現在我們按上一節的步驟分析可執行檔案max
,看看連結器都做了什麼改動。
$ readelf -a max 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: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x8048074 Start of program headers: 52 (bytes into file) Start of section headers: 256 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 2 Size of section headers: 40 (bytes) Number of section headers: 6 Section header string table index: 3 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 08048074 000074 00002a 00 AX 0 0 4 [ 2] .data PROGBITS 080490a0 0000a0 000038 00 WA 0 0 4 [ 3] .shstrtab STRTAB 00000000 0000d8 000027 00 0 0 1 [ 4] .symtab SYMTAB 00000000 0001f0 0000a0 10 5 6 4 [ 5] .strtab STRTAB 00000000 000290 000040 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) There are no section groups in this file. Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x08048000 0x08048000 0x0009e 0x0009e R E 0x1000 LOAD 0x0000a0 0x080490a0 0x080490a0 0x00038 0x00038 RW 0x1000 Section to Segment mapping: Segment Sections... 00 .text 01 .data There is no dynamic section in this file. There are no relocations in this file. There are no unwind sections in this file. Symbol table '.symtab' contains 10 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 08048074 0 SECTION LOCAL DEFAULT 1 2: 080490a0 0 SECTION LOCAL DEFAULT 2 3: 080490a0 0 NOTYPE LOCAL DEFAULT 2 data_items 4: 08048082 0 NOTYPE LOCAL DEFAULT 1 start_loop 5: 08048097 0 NOTYPE LOCAL DEFAULT 1 loop_exit 6: 08048074 0 NOTYPE GLOBAL DEFAULT 1 _start 7: 080490d8 0 NOTYPE GLOBAL DEFAULT ABS __bss_start 8: 080490d8 0 NOTYPE GLOBAL DEFAULT ABS _edata 9: 080490d8 0 NOTYPE GLOBAL DEFAULT ABS _end No version information found in this file.
在ELF Header中,Type
改成了EXEC
,由目標檔案變成可執行檔案了,Entry point address
改成了0x8048074(這是_start
符號的地址),還可以看出,多了兩個Program Header,少了兩個Section Header。
在Section Header Table中,.text
和.data
段的載入地址分別改成了0x08048074和0x080490a0。.bss
段沒有用到,所以被刪掉了。.rel.text
段就是用於連結過程的,做完連結就沒用了,所以也刪掉了。
多出來的Program Header Table描述了兩個Segment的資訊。.text
段和前面的ELF Header、Program Header Table一起組成一個Segment(FileSiz
指出總長度是0x9e),.data
段組成另一個Segment(總長度是0x38)。VirtAddr
列指出第一個Segment載入到虛擬地址0x08048000(注意在x86平臺上後面的PhysAddr
列是沒有意義的,並不代表實際的實體地址),第二個Segment載入到地址0x080490a0。Flg
列指出第一個Segment的訪問許可權是可讀可執行,第二個Segment的訪問許可權是可讀可寫。最後一列Align
的值0x1000(4K)是x86平臺的記憶體頁面大小。在載入時檔案也要按記憶體頁面大小分成若干頁,檔案中的一頁對應記憶體中的一頁,對應關係如下圖所示。
這個可執行檔案很小,總共也不超過一頁大小,但是兩個Segment必須載入到記憶體中兩個不同的頁面,因為MMU的許可權保護機制是以頁為單位的,一個頁面只能設定一種許可權。此外還規定每個Segment在檔案頁面內偏移多少載入到記憶體頁面仍然要偏移多少,比如第二個Segment在檔案中的偏移是0xa0,在記憶體頁面0x08049000中的偏移仍然是0xa0,所以從0x080490a0開始,這樣規定是為了簡化連結器和載入器的實現。從上圖也可以看出.text
段的載入地址應該是0x08048074
,_start
符號位於.text
段的開頭,所以_start
符號的地址也是0x08048074,從符號表中可以驗證這一點。
原來目標檔案符號表中的Value
都是相對地址,現在都改成絕對地址了。此外還多了三個符號__bss_start
、_edata
和_end
,這些符號在連結指令碼中定義,被連結器新增到可執行檔案中。
再看一下反彙編的結果:
$ objdump -d max max: file format elf32-i386 Disassembly of section .text: 08048074 <_start>: 8048074: bf 00 00 00 00 mov $0x0,%edi 8048079: 8b 04 bd a0 90 04 08 mov 0x80490a0(,%edi,4),%eax 8048080: 89 c3 mov %eax,%ebx 08048082 <start_loop>: 8048082: 83 f8 00 cmp $0x0,%eax 8048085: 74 10 je 8048097 <loop_exit> 8048087: 47 inc %edi 8048088: 8b 04 bd a0 90 04 08 mov 0x80490a0(,%edi,4),%eax 804808f: 39 d8 cmp %ebx,%eax 8048091: 7e ef jle 8048082 <start_loop> 8048093: 89 c3 mov %eax,%ebx 8048095: eb eb jmp 8048082 <start_loop> 08048097 <loop_exit>: 8048097: b8 01 00 00 00 mov $0x1,%eax 804809c: cd 80 int $0x80
指令中的相對地址都改成絕對地址了。我們仔細檢查一下改了哪些地方。首先看跳轉指令,原來目標檔案的指令是這樣:
... 11: 74 10 je 23 <loop_exit> ... 1d: 7e ef jle e <start_loop> ... 21: eb eb jmp e <start_loop> ...
現在改成了這樣:
... 8048085: 74 10 je 8048097 <loop_exit> ... 8048091: 7e ef jle 8048082 <start_loop> ... 8048095: eb eb jmp 8048082 <start_loop> ...
改了嗎?其實只是反彙編的結果不同了,指令的機器碼根本沒變。為什麼不用改指令就能跳轉到新的地址呢?因為跳轉指令中指定的是相對於當前指令向前或向後跳多少位元組,而不是指定一個完整的記憶體地址,記憶體地址有32位,這些跳轉指令只有16位,顯然也不可能指定一個完整的記憶體地址,這稱為相對跳轉。這種相對跳轉指令只有16位,只能在當前指令前後的一個小範圍內跳轉,不可能跳得太遠,也有的跳轉指令指定一個完整的記憶體地址,可以跳到任何地方,這稱絕對跳轉.
再看記憶體訪問指令,原來目標檔案的指令是這樣:
... 5: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax ... 14: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax ...
現在改成了這樣:
... 8048079: 8b 04 bd a0 90 04 08 mov 0x80490a0(,%edi,4),%eax ... 8048088: 8b 04 bd a0 90 04 08 mov 0x80490a0(,%edi,4),%eax ...
指令中的地址原本是0x00000000,現在改成了0x080490a0(注意是小端位元組序)。那麼連結器怎麼知道要改這兩處呢?是根據目標檔案中的.rel.text
段提供的重定位資訊來改的:
... Relocation section '.rel.text' at offset 0x2b0 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000008 00000201 R_386_32 00000000 .data 00000017 00000201 R_386_32 00000000 .data ...
第一列Offset
的值就是.text
段需要改的地方,在.text
段中的相對地址是8和0x17,正是這兩條指令中00 00 00 00的位置。