簡介 程式設計師很大的機率是脫離不了Linux,而如果我們要在Linux上compile,大概一定會接觸到ELF這個格式。底下來簡單介紹一下ELF的格式是什麼,我們要怎麼從它獲得資訊。
ELF全名是Executable and Linking Format,在Linux中是編譯後的binary、object檔規範,也就是說我們從source code編譯後產生的檔案格式就是ELF了。
ELF的格式可以從兩種角度來看,第一種是Link的時候,第二種是執行的時候。兩者都一樣會有ELF header,但是底下的組成概念就完全不一樣。
Link的時候:
ELF header
Program Header Table(Optional)
Section 1
Section 2
…
Section N
Section Header Table
執行的時候:
ELF header
Program Header Table
Segment 1
Segment 2
…
Segment N
Section Header Table(Optional)
兩者最大的差異是Link的時候是以Section為觀點,用Section Header Table來當索引,指向各個Section。執行的時候則是用Segment為觀點,一個Segment可能是多個Section所組成,然後再用Program Header Table指向各個Segment。
觀察ELF的方法 那要如何觀察ELF呢?如果你嘗試用記事本打開應該只會看到一團不知所云的亂碼,所以我們底下會透過各種工具的使用教學來解釋ELF格式。
查看執行檔 - od 首先我們可以試著使用od這個指令來看檔案內容。od全名是octal dump,顧名思義就是用八進制來印內容,但他並不僅僅如此而已。
od指令的格式:od -t [顯示格式] -A [偏移量使用的基數] [filename]
-t:後面可接型態(d, o, x…)、一次顯示的byte數(數字)、是否顯示ASCII code(z)
-A:偏移量有(d, o, x, n),n代表不顯示偏移量
-v:不省略重複的內容
我們最常用格式:
od -t x1 -A x [filename]
:代表用16進制來顯示檔案,偏移量是16的倍數
od -t x1z -A x [filename]
:同上,但是多加上顯示ASCII code
那我們來看看ELF檔長什麼樣子,這邊以大家最常用的ls為例
1 2 3 4 5 $ od -t x1z -A x /bin/ls | less 000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 >.ELF............< 000010 02 00 3e 00 01 00 00 00 90 48 40 00 00 00 00 00 >..>......H@.....< 000020 40 00 00 00 00 00 00 00 00 a7 01 00 00 00 00 00 >@...............< ....
可以看到前面有個7f 45 4c 46
開頭,ASCII是.ELF
(.代表非可見字元,這邊是0x7f也就是\177),這個就是傳說中的ELF magic code了。不過這邊先停一下,如果我們要繼續用hex來看其實有點累,所以先換個工具來試試吧!
使用readelf來觀察ELF資訊 readelf很明顯就是觀察ELF檔案的專門工具,使用方式如下
格式:readelf [選項] [filename]
讀取標頭選項
-h:印 ELF header
-l:印 Program Header Table
-S:印 Section Header Table
-e:三者都印
讀取資訊選項
特別用法:
-a:所有標頭資訊全部印出
-xn:先用-S看要查看的Section數字,然後n填上該數字就可以hexdump那個section
那我們來看看ls的ELF header長什麼樣。從下面可以看到,除了剛剛看到的Magic code外,還有版本、適用哪個OS/ABI、在哪個機器平台運行、entry point adddress等等。
值得注意的是這邊有紀錄Program Header、Section Header的開始位址、大小、數量,所以我們可以用這個資訊找到Program/Section Header。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $ readelf -h /bin/ls ELF 檔頭: 魔術位元組: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 類別: ELF64 資料: 2 的補數,小尾序(little endian) 版本: 1 (current) OS/ABI: UNIX - System V ABI 版本: 0 類型: EXEC (可執行檔案) 系統架構: Advanced Micro Devices X86-64 版本: 0x1 進入點位址: 0x404890 程式標頭起點: 64 (檔案內之位元組) 區段標頭起點: 108288 (檔案內之位元組) 旗標: 0x0 此標頭的大小: 64 (位元組) 程式標頭大小: 56 (位元組) Number of program headers: 9 區段標頭大小: 64 (位元組) 區段標頭數量: 28 字串表索引區段標頭: 27
而Program Header的部分,我們可以看到有9個Segement,以及實際的位址在哪。另外有個「區段到節區映射中」(Section to Segment mapping),這就是多個Section如何組成一個Segment的對應。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 $ readelf -l /bin/ls Elf 檔案類型為 EXEC (可執行檔案) 進入點 0x404890 共有 9 個程式標頭,開始於偏移量 64 程式標頭: 類型 偏移量 虛擬位址 實體位址 檔案大小 記憶大小 旗標 對齊 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 0x0000000000019d44 0x0000000000019d44 R E 200000 LOAD 0x0000000000019df0 0x0000000000619df0 0x0000000000619df0 0x0000000000000804 0x0000000000001570 RW 200000 DYNAMIC 0x0000000000019e08 0x0000000000619e08 0x0000000000619e08 0x00000000000001f0 0x00000000000001f0 RW 8 NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254 0x0000000000000044 0x0000000000000044 R 4 GNU_EH_FRAME 0x000000000001701c 0x000000000041701c 0x000000000041701c 0x000000000000072c 0x000000000000072c R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 10 GNU_RELRO 0x0000000000019df0 0x0000000000619df0 0x0000000000619df0 0x0000000000000210 0x0000000000000210 R 1 區段到節區映射中: 節區段... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gn u.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_ frame_hdr .eh_frame 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 08 .init_array .fini_array .jcr .dynamic .got
Section Header的話會仔細列出這個ELF所包含的所有Section以及位址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 $ readelf -S /bin/ls 共有 28 個區段標頭,從偏移量 0x1a700 開始: 區段標頭: [號] 名稱 類型 位址 偏移量 大小 全體大小 旗標 連結 資訊 對齊 [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000400238 00000238 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.ABI-tag NOTE 0000000000400254 00000254 0000000000000020 0000000000000000 A 0 0 4 [ 3] .note.gnu.build-i NOTE 0000000000400274 00000274 0000000000000024 0000000000000000 A 0 0 4 [ 4] .gnu.hash GNU_HASH 0000000000400298 00000298 0000000000000068 0000000000000000 A 5 0 8 ... [24] .data PROGBITS 000000000061a3a0 0001a3a0 0000000000000254 0000000000000000 WA 0 0 32 [25] .bss NOBITS 000000000061a600 0001a5f4 0000000000000d60 0000000000000000 WA 0 0 32 [26] .gnu_debuglink PROGBITS 0000000000000000 0001a5f4 0000000000000008 0000000000000000 0 0 1 [27] .shstrtab STRTAB 0000000000000000 0001a5fc 00000000000000fe 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)
objdump取得ELF內容 除了看ELF內的資訊外,我們可以進一步得到更細的資訊,包括dump內容和反組譯程式,這時候就要用objdump了
objdump -s -j [section] [filename]
:把特定section完整dump出來
objdump -h [filename]
:看有哪些section,跟readelf功用類似
objdump -x [filename]
:把所有section都顯示出來
objdump -d [filename]
:反組譯程式
objdump -d -S [filename]
:反組譯程式加上行數
objdump -d -l [filename]
:反組譯程式加上source code
同樣以ls為例,可以看到我們把text section的內容印出來了
1 2 3 4 5 6 7 8 9 $ objdump -s -j .text /bin/ls /bin/ls: 檔案格式 elf64-x86-64 Contents of section .text: 4028a0 50b9882c 4100baa6 0e0000be 36374100 P..,A.......67A. 4028b0 bf983c41 00e896fb ffff660f 1f440000 ..<A......f..D.. 4028c0 41574156 41554154 554889f5 5389fb48 AWAVAUATUH..S..H ....
objcopy/strip修改ELF檔案 objcopy最主要的功能就是可以把文件作轉換,一部份或全部的內容copy另一個文件中
objcopy -S -R .comment -R .note [input filename] [output filename]
:把編譯出來的symbol移除不必要的section(-S代表去掉symbol, relocation的訊息)
objcopy -O binary -j [section] [input filename] [output filename]
:也可以把某個section拿出來
關於移除不必要的section部分,其實strip就可以做到了,只要用strip [filename]
即可。
objcopy進階用法 objcopy可以做到把檔案變成ELF格式,提供給我們linking,這樣我們就可以避免檔案的讀取。
這邊用個簡單的範例,假設我們想要把某個文字檔包在程式內部(其實可以用圖片比較有感覺,只是我不想寫太複雜的程式)
先創立text.txt
然後把text.txt變成object file
1 objcopy -I binary -O elf64-x86-64 -B i386:x86-64 text.txt text.o
如果這時候show object資訊的話
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ objdump -x text.o text.o: 檔案格式 elf64-x86-64 text.o 系統架構:i386:x86-64, 旗標 0x00000010: HAS_SYMS 起始位址 0x0000000000000000 區段: 索引名稱 大小 VMA LMA 檔案關閉 對齊 0 .data 00000012 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, DATA SYMBOL TABLE: 0000000000000000 l d .data 0000000000000000 .data 0000000000000000 g .data 0000000000000000 _binary_test_txt_start 0000000000000012 g .data 0000000000000000 _binary_test_txt_end 0000000000000012 g *ABS* 0000000000000000 _binary_test_txt_size
symsymbola把下面那些symbol放入test.c內,即可使用
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdlib.h> extern char _binary_text_txt_start[]; extern char _binary_text_txt_end[]; extern char _binary_text_txt_size[]; int main() { char *ptr = _binary_text_txt_start; printf("text.txt=%s\r\n", ptr); return 0; }
編譯並執行
1 2 3 4 $ gcc test.o test.o -o a.out % ./a.out text.txt=This is test txt.
nm觀察symbol 剛剛提了那麼多都是以ELF內的各種section為主,但是我們實際開發程式其實還是比較重視symbol,那我們有簡單方式可以看symbol嗎?這時候就要用到nm了。
nm [filename]
:可以顯示symbol的數值、型態、名稱
nm --size-sort -r -S [filename]
:由大到小顯示symbol的數值、大小、型態、名稱
舉個例子,我們可以看到下面執行結果symbol由大到小排序
1 2 3 4 5 6 7 $ nm --size-sort -r -S test 00008464 00000064 T __libc_csu_init 00008444 00000020 T main 000084c8 00000004 T __libc_csu_fini 000084d4 00000004 R _IO_stdin_used 00011028 00000001 b completed.9228
關於常見型態的部分可參考下表:
Section
類型(大寫代表global、小寫是local)
text section
T/t
data section
D/d
Read only
R/r
BSS
B/b
未定義(如extern)
U
addr2line從位址轉成symbol 有時候我們執行程式會只知道位址,但是想要從位址得到到底是在程式哪行掛掉
addr2line -f -e [filename] [address]
:-f代表要顯示是哪個function,-e代表address是來自該執行檔
總結 本篇文章主要簡單介紹ELF的結構,然後我們可以用 od、readelf、objdump、objcopy/strip、nm, addr2line 幾個工具觀察ELF的格式。如果想要有進一步的認識,建議可以研究參考的連結。
參考