Understanding ELF using readelf and objdump

來源:互聯網
上載者:User

譯者說明:
文章的原文地址:
http://www.linuxforums.org/articles/understanding-elf-using-readelf-and-objdump_125.html
這是我第一次翻譯技術類文章,可能會有一些錯別字,尤其一些語句不通的地方。這可能是由於我用五筆打字,形成的筆誤,請大家批評指正。
【】中的內容,是譯者作的注,注意:xxxx。斜體字是作者的注。


通過readelf和objdump學習ELF

首先,你應該瞭解一下elf 目標檔案三種形式:

l 可重新導向檔案:這種檔案持有代碼(code)和資料,需要與其它的目標檔案link在一起,來產生一個可執行檔檔案或是一個共用庫檔案。換而言之,你可是把可重新導向檔案理解為:它是產生可執行檔和庫的基礎。

如果你如下方式編譯原始碼,就可得到這種檔案:

$gcc -c test.c

這會產生一個test.o,它就是可重新導向檔案。

核心模組(如*.o或是*.ko)都是可重新導向檔案的形式。

l 可執行檔:此目標檔案持有可執行檔程式(program,這是可執行檔二進位程式碼群組成的),例如:你的mp3播放器,你的VCD軟體播放器,甚至你的 txt的編輯器,這都是elf的可執行檔。

你編譯一個程式,即可得到類似的檔案:

$ gcc -o test test.c

在你確認“test”程式的可執行位是啟用時(linux中檔案的可執行位),你就可以執行它了。有個問題:shell 指令碼是怎麼執行的呢?Shell指令碼不是一個elf可執行檔,它只是一個解譯器。

l 共用庫檔案:此檔案持有代碼和資料,但是用在兩種不同的用法。

1. Link editor可以把它+其它的可重新導向檔案+共用庫檔案一起處理,進而產生一個其它的目標檔案。【這就是將靜態庫與其它的*.o編譯在一起的方式】

2. Dynamic linker(動態連接器)把它、可執行檔、其它庫結合在一起,進而產生一個process image(進程鏡像)。

一句話:這些檔案,就是你常見到的*.so檔案。(一般在/usr/lib中)

還有其它的方式來確定elf檔案的類型嗎?當然有。在每一個elf檔案中,有一個檔案頭,它中的欄位表示了此檔案的類型。假如你要建立一個二進位包,你可以使用readelf命令,來讀出這個頭。例如(命令結果會適當瘦身只顯示相關的域資訊):

$ readelf -h /bin/ls Type:EXEC (Executable file)

sn@ubuntu:~$readelf -h /bin/ls

ELFHeader:

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

ABIVersion: 0

Type: EXEC (Executable file)

Machine: Intel 80386

Version: 0x1

Entrypoint address: 0x8049d60

Startof program headers: 52 (bytes into file)

Startof section headers: 95164 (bytes into file)

Flags: 0x0

Sizeof this header: 52 (bytes)

Sizeof program headers: 32 (bytes)

Numberof program headers: 9

Sizeof section headers: 40 (bytes)

Numberof section headers: 29

Sectionheader string table index: 28

$ readelf -h /usr/lib/crt1.o Type: REL (Relocatable file)

sn@ubuntu:~$ readelf -h/usr/lib/crt1.o

ELFHeader:

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

ABIVersion: 0

Type: REL (Relocatable file)

Machine: Intel 80386

........

$ readelf -h /lib/libc-2.3.2.so Type: DYN (Shared object file)

sn@ubuntu:~$readelf -h /lib/libcap.so.2

ELFHeader:

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

ABIVersion: 0

Type: DYN (Shared object file)

Machine: Intel 80386

......

“File”命令不適當查看目標檔案資訊,我不想多說這個,讓我們關注readelf和objdump。現在我們就開始學習它們。

為了讓我們更輕鬆地學習ELF,你可以使用以下簡單的C程式:

/*test.c */#includeint global_data = 4;int global_data_2;int main(int argc, char **argv){int local_data = 3;    printf("HelloWorldn");    printf("global_data= %dn",   global_data);     printf("global_data_2= %dn", global_data_2);     printf("local_data= %dn", local_data);    return(0);}

並且編譯它:

$gcc -o test test.c

A.查看ELF的頭。

剛產生的二進位就是我們要查看的目標。讓我們從ElF的頭開始吧:

$readelf -h test

ELFHeader:

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

ABIVersion: 0

Type:EXEC (Executable file)

Machine:Intel 80386

Version:0x1

Entrypoint address: 0x80482c0

Startof program headers: 52 (bytes into file)

Startof section headers: 2060 (bytes into file)

Flags:0x0

Sizeof this header: 52 (bytes)

Sizeof program headers: 32 (bytes)

Numberof program headers: 7

Sizeof section headers: 40 (bytes)

Numberof section headers: 28

Sectionheader string table index: 25

從這個頭是告訴我們什麼呢?

這個可執行檔是可以在Intel x86 32 bit的體系的機器上啟動並執行(從“machine”和“class”欄位)。

當執行時,程式將從虛地址0x080482c0(看“Entry point address”)開始運行。這個地址不是指向我們常見的main()函數地址的,但是它指向是一個名為__start的函數。你從未感覺到建立了它是嗎?當然你沒有,__start函數是被linker建立的,它的目標是初始你的程式。

這個程式還有28個節區(section)和7個段(segment)【最近讀了一些有關ELF的文章,有的把section翻譯成“段”,有也把segment翻譯成“段”,大家在讀文章時要注意】。

什麼是節區(section)? Section是在目標檔案中的一個區,它包括一些資訊(這些資訊對串連過程有用):程式的代碼、程式的資料(變數、數組、字串),可重新導向的資訊和其它。所以,在每一個區,幾種資訊組合在一起,這裡有一個明顯地含義:代碼區只有代碼,資料區只是初始化的或是沒有初始化的資料,等等。節區頭部分列表(Section Header Table,SHT)精確地告訴我們:ELF目標檔案中有什麼section。至少從“Number of section headers”欄位中知道“test”目標檔案有28個section.

如果section是一個二進位表示的,我們的linux核心不能用一種方式讀懂它,linux核心準備幾個VMA(Virtual Memory Area),它們包括虛擬位址連續的頁面幀。在VMA的內部,一個或多個section被映射其中。在這個例子中每一個VMA都代表一個ELF的段(segment)。那核心是如何知道哪個section去往哪個segment呢?這是Program Header Table(PHT)的工作。

 ELF結構的兩種不同示圖。

B.查看Section Header Table(SHT)

讓我們看一個Section在程式中的存在形式:

$ readelf -S test

Thereare 28 section headers, starting at offset 0x80c:

SectionHeaders:

[Nr] Name  Type          Addr           Off          Size     ES Flg Lk Inf Al

[4] .dynsym DYNSYM 08048174 000174 000060  10  A   5  1   4

........

[11].plt PROGBITS 08048290 000290 000030      04 AX  0  0   4

[12].text PROGBITS 080482c0 0002c0 0001d0 00 A0 0 4

........

[20].got PROGBITS 080495d8 0005d8 000004 04 WA 0 0 4

[21].got.plt PROGBITS 080495dc 0005dc 000014 04 WA 0 0 4

........

[22].data PROGBITS 080495f0 0005f0 000010 00 WA 0 0 4

[23].bss NOBITS 08049600 000600 000008 00 WA 0 0 4

........

[26].symtab SYMTAB 00000000 000c6c 000480 10 27 2c 4

........

編譯器把可執行代碼儲存到.text節區中。那.text節區被標記為可執行('X'在flag欄位)。在這個節區,你可以看到我們main()函數的機器代碼。

$ objdump -d -j.text test

-d選項告訴objdump分解機器代碼。-j告訴objdump只關心那個特定的節區(在本例中,是.text)。以下是執行命令後的部分內容。

08048370 :.......

8048397: 83 ec 08sub $0x8,%esp

804839a: ff 35 fc95 04 08 pushl 0x80495fc

80483a0: 68 c1 8404 08 push $0x80484c1

80483a5: e8 06 ffff ff call 80482b0

80483aa: 83 c4 10add $0x10,%esp

80483ad: 83 ec 08sub $0x8,%esp

80483b0: ff 35 0496 04 08 pushl 0x8049604

80483b6: 68 d3 8404 08 push $0x80484d3

80483bb: e8 f0 feff ff call 80482b0 .......

.data節區儲存所有的初始化的變數,這些變數不在棧中。“Initialized”是指這些變數被賦於初始值,如”global_data”。那”local_data”呢?“local_data”的值不在此節區中,它們生活在進程的棧裡。

以下是用objdump查看.data節區:

$ objdump -d -j.data test

.....

080495fc <global_data>:

80495fc: 04 00 00 00 .........  【此處作了修改。】

我們可推斷出objdump可以很好地完成地址與符號之間的轉譯工作。不用在符號表中找,我們可以知道080495fc【作者原文是0x08049424】是global_data的地址。這裡我們可以看到它的初始值為4。請註解linux建立的通用可執行檔。這裡沒有注釋的符號表。Objdump很難解析這個地址。

那.bss呢?BSS(BlockStarted by Symbol)是一個映射【注意是映射,不是儲存,這個節區在目標檔案中的size為0,但在進程中,這個區是有實際空間的,也就是說初始化的變數在進程運行時被建立,在靜態程式中是沒他們的空間】未初始設定變數的節區,你可能會想“每個東東都應該有明確地初始值”。誠然,在linux中,所有的未初始化的變數都被設定為0。這也是為什麼.bss只是一片0的原因。對於字元類型的變數,那就是null字元。知道這個事實,我們知道在運行時,global_data_2被迫成為0.

$objdump -d -j .bss test

Disassemblyof section .bss:

.....

08049604: 8049604: 00 00 00 00 .........

之前,我們提到了符號表。這個表可以找到符號名(不能是外部函數和變數)與地址的關聯關係。使用-s,readelf可以解調這個符號表。

$readelf -s ./test

Symboltable '.dynsym' contains 6 entries:

Num:Value Size Type Bind Vis Ndx Name

.....

2:00000000 57 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.0 (2)

.....

Symboltable '.symtab' contains 72 entries:

Num:Value Size Type Bind Vis Ndx Name

.....

49:080495fc 4 OBJECT GLOBAL DEFAULT 22 global_data

.....

55:08048370 109 FUNC GLOBAL DEFAULT 12 main

.....

59:00000000 57 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.0

.....

61:08049604 4 OBJECT GLOBAL DEFAULT 23 global_data_2

.....

"value" 指示了符號對應的地址。例如:如果一個指令引用的地址(pushl 0x80495fc),此地址含義是global_data。對Printf()這個符號處理是不同的,因為這個符號是外部函數的符號。要知道printf是 定義在了glibc中,不是在test程式的內部,之後呢,我會解釋我們的test程式是如何調用到printf的。

C.查看 program Header Table(PHT)

像我之前解釋的方法,段(segment)是一個OS“看懂”我們程式的方法。讓我們看看我們程式是如何變成段的吧:

$readelf -l test

here are 7 program headers, starting at offset 52Program Headers: 

     Type    Offset   VirtAddr    PhysAddr    FileSiz   MemSiz  Flg Align

[00] PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4

[01] INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1

[02] LOAD 0x000000 0x08048000 0x08048000 0x004fc 0x004fc R E 0x1000

[03] LOAD 0x0004fc 0x080494fc 0x080494fc 0x00104 0x0010c RW 0x1000

[04] DYNAMIC 0x000510 0x08049510 0x08049510 0x000c8 0x000c8 RW 0x4

[05] NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4

[06] STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4

Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag

06

注意:我自己給這些輸出增加了[x]的行號。在實際的輸出中是沒有的。

這個映射很直觀。例如段號2,這裡有15個節區被映射到其中。.text節區就映射到此段。它的標誌是R,E其含義分別是可讀,可執行。W 就是可讀的含義。

看 一下“VirtAddr“一列,我們能發現這是每一個段的虛擬首地址。看一個2號段,它的首地址是0x08048000。在這個節區,我們可發現這個地址 不是段在記憶體中的真真實位址。你先忽略"PhyAddr",因為linux一直運行在儲存模式(在Intel/AMD 32 bit 和64bit)所以這個虛擬位址是我們關心的。

段有很多類型,我們只關心兩類:

  • LOAD : 這種段的內容是從可執行檔中載入進來的。"offset"指示了核心應該從檔案哪個位置開始讀取。"FileSiz"告訴我們從該檔案讀多少位元組。例如:2號段(segment)它的內容是從檔案0到0x4fc的這段內容。為了迅速的執行,當有需要時,檔案的內容才被讀到記憶體中。【這裡的LOAD是指映射到使用者的虛擬空間,不是將資料複製到相應的物理頁面。】
  • STACK: 這個段是棧的地區。有意思的是它的欄位全是0,除了"Flg"和“Align"。不會是錯了吧?不是的。決定棧的開始的地址,以及它的大小這都是核心的工作。請記住:在intel的CPU,棧是向下增長的(地址遞減說明是在進棧)。

 很好奇看到程式段的真實的布局是嗎?我們使用/proc/<pid>/maps 檔案也可以得看到它。<pid>是一個我們想要查看的進程的ID。要行動之前,還有一個小問題,我們的test進程啟動並執行太快了,在我們進入/proc這前,它就結束了。我使用gdb來解決此問題。你也可以在return之前調用 sleep()來搞定這個問題。

在另一個控制台中(或是類比的終端如xterm):

$ gdb test

(gdb) b main

Breakpoint 1 at 0x8048376

(gdb) r

Breakpoint 1, 0x08048376 in main () 

在此保持(hold)住,開啟另一個控制台,找到test的PID。如果你想圖省事的話,就這樣:

$ cat /proc/`pgrep test`/maps 

你將看到如下的輸出:(你的輸出可能有點不同)

[1] 0039d000-003b2000 r-xp 00000000 16:41 1080084 /lib/ld-2.3.3.so

[2] 003b2000-003b3000 r--p 00014000 16:41 1080084 /lib/ld-2.3.3.so

[3] 003b3000-003b4000 rw-p 00015000 16:41 1080084 /lib/ld-2.3.3.so

[4] 003b6000-004cb000 r-xp 00000000 16:41 1080085 /lib/tls/libc-2.3.3.so

[5] 004cb000-004cd000 r--p 00115000 16:41 1080085 /lib/tls/libc-2.3.3.so

[6] 004cd000-004cf000 rw-p 00117000 16:41 1080085 /lib/tls/libc-2.3.3.so

[7] 004cf000-004d1000 rw-p 004cf000 00:00 0

[8] 08048000-08049000 r-xp 00000000 16:06 66970 /tmp/test

[9] 08049000-0804a000 rw-p 00000000 16:06 66970 /tmp/test

[10] b7fec000-b7fed000 rw-p b7fec000 00:00 0

[11] bffeb000-c0000000 rw-p bffeb000 00:00 0

[12] ffffe000-fffff000 ---p 00000000 00:00 0

注意:我自己給這些輸出增加了[x]的行號。在實際的輸出中是沒有的。

【是譯者的用例】

回到gdb,輸入:

(gdb) q

於是,最後,我們看到了12個段(實際上是VMA)。重點關注第一個欄位和最後一欄位。第一欄位顯示了VMA的位址範圍,最後一個欄位顯示了背後的檔案。你在看到VMA的第8行與之前PHT的第2行的類似點了嗎?不同之處是SHT說它自己於0x080484fc結束,但在8號段中我們看到它的結束位址是0x08049000。在VMA9號與段3號之間也有同樣的現象。SHT顯示3號段開始於0x080494fc。而VMA則顯示開始於0x08049000。

有這麼幾個因素我們必須瞭解:

1.儘管VMA開始於不同的地址,與之關聯的節區仍然被映射到精確的虛擬位址上了。

2.核心分配記憶體是以4KB的頁為基本單位的,所以每一頁的地址都是4KB的整數倍。如0x1000,0x2000等。對於VMA的9號,這個頁的地址是0x08049000。或從技術角度講,這個段的地址必須與頁面的大小對齊。

最後,哪一個VMA是棧呢?VMA11就是。一般地,核心動態地分配幾個頁面,並映射到使用者空間可能的最高的虛擬位址,這就是棧的地區了。簡單地講,每一個進程的地址空間被分成兩部分(前提是32位的CPU):使用者空間和核心空間。使用者空間在0x00000000-0xc0000000 ,所以核心空間只能在0xc0000000以上了。

於是,分配給棧的地址是在0xc0000000邊界附近的。結束位址是固定的,開始地址可以根據儲存內容的多少而變化。

D.一個函數是怎麼一回事呢?

一個程式(它自己是可執行檔)調用一個函數。它要做的很簡單:只是調用一個過程(函數)。但是如果它調用了如printf()這樣定義在glibc庫中的函數會怎麼樣呢?

這裡,我們不深入地討論動態連結器是如何工作的,我重點講一個在可執行體(或是可執行檔或是進程)中,調用機制是怎麼玩的。有了這個前提,讓我們繼續。

當一個程式想到調用一個函數時,它得按以下的流程來做:

1.它得完成一個跳躍(jump),跳到在PLT(Procedure Linkage Tabe)中要調用的函數相關的條目。

2.在PLT中,還有一個跳躍,跳躍到在GOT(Global Offset Table)中的相關條目的地址。

3.如果這個函數是第一次被調用,則進入第4步,否則進入第5步。

4.相關的GOT入口包含了一個地址(指向PLT下一條指令的地址點)。程式將會跳到此地址,並且調用動態連結器,讓它搞定函數的地址。如果函數地址找到了,這個地址被放入相關的GOT條目中,最後這個函數被執行。

於是,當再一次調用此函數時,GOT已經持有了它的地址,PLT就直接跳到這個地址上了。這個過程叫做懶綁定【到了真正用的時候,才完成綁定過程的機制叫懶綁定】;所有的外部符號直到它們第一次被真實地需要時,這些符號才被轉譯成地址。(在這個例子中,就是函數被調用時,函數符號才被轉譯成地址)。現在轉到第6步。

5.跳到GOT提及的地址點。這個地址點就是函數的地址。不需要再經過動態連結器了。

6.執行完成函數之後,跳回到調用者的下一條指令。

一般地,查看可執行檔的內容的最好方式就是反解析它。可以這樣:

$ objdump -d -j .text test 

你就可以看到如下代碼了:

.....08048370 :..... 804838f: e8 1c ff ff ff call 80482b0 【這是進入PLT的條目的地址。】

我們在0x80482b0處幹什麼了:

080482b0 : 【PLT表,每一個表項有16個位元組,每個表項是一段彙編代碼。】

80482b0: ff 25 ec 95 04 08 jmp *0x80495ec    【這個地址是GOT表的一個表項的地址,這個GOT表項對應PLT表項】

80482b6: 68 08 00 00 00 push $0x8 

80482bb: e9 d0 ff ff ff jmp 8048290 <_init+0x18>

你看,在0x80482b0處是一個間接跳轉*0x80495ec(*要在地址之前)。所以,看它跳哪去。我們得再看0x80482b0一下。猜想,這個地址要麼在.GOT中,要麼在.GOT.PLT中。回頭看SHT,我們在.GOT.PLT中找到了它。我使用readelf完成十六進位的轉置。

$ readelf -x 21 test

Hex dump of section '.got.plt': 

0x080495dc 080482a6 00000000 00000000 08049510 

................ 

0x080495ec 080482b6 ....

注意,第一列是虛擬位址,這個地址上的資料在第5列,不是第二列。

有了!我們的"080482b6"就在這裡。換句話說,我們回到了PLT【回到了相應PLT表項中的第二條指令push $0x8】,在這裡我們跳轉到了另一個地址。這裡的工作是由動態連結器在一開始就完成了的,所以我們把它略過了。假設動態連結器已經完成了這個工作,在GOT的一個條目中持有了printf函數的地址。

E.其它的檢測elf結構的工具

除了readelf和objdump,還有一個工具叫Beye。它是一個檔案的查看器,能解析ELF結構。你可以從http://beye.sourceforge.net 得到源檔案,並自己編譯它。

一般,Beye 會在Linux的live CD中。

我個人比較喜歡Beye,因為它提供一個GUI 的顯示。針對節區有導航,可以查看ElF頭,列出符號表或是其它的任務,這些都只要你點幾下鍵盤就搞定了。

例如:你能列出符號,並直接跳轉到符號的地址。我們試著跳轉到main函數。第一步,啟動Beye.

$ beye test

先按F7後,再按Ctrl+A可查看到符號表。為了節省時間,再按下F7來開啟"Find string"菜單。輸入"main"按下斷行符號。那個高亮的條目就是你要找的。輕鬆按下斷行符號,Beye就跳轉到main的地址了。不要忘了切換到彙編模式(按F2來選擇),這樣你能看到機器碼的進階形式(彙編形式)。

是Beye列出的符號。

一般我們更希望看到虛擬位址,不是檔案的位移。切換到虛擬位址視圖更好些。先按F6再按Ctrl+C,選擇"Local"。你可以看到最左列是虛擬位址

總結

這個文章只是學習ELF結構的簡介。使用readelf和objdump,你就可以開始上道了。如有需要,可以使用Beye工具,它能協助你快速地探索內部二進位。把你所學的東東,用會,用熟,那你成這方面的大師了。

進一步閱讀:

http://www.linuxjournal.com/article/1059
http://www.linuxjournal.com/article/1060

由Eric Youngdale編寫的很不錯的ELF介紹性文章。

http://en.wikipedia.org/wiki/Executable_and_Linkable_Format

ELF在Wikipedia上的說明,從那裡,你能找到一些其它有用的文章。

http://en.wikipedia.org/wiki/Executable_and_Linkable_Format

這個文檔完整地詳細地說明了ELF的結構,讀完本文之後再讀此文,可以對ELF有一個全面的瞭解。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.