青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

tqsheng

go.....
隨筆 - 366, 文章 - 18, 評論 - 101, 引用 - 0
數(shù)據(jù)加載中……

可執(zhí)行文件(ELF)格式的理解

ELF(Executable and Linking Format)是一種對象文件的格式,用于定義不同類型的對象文件(Object files)中都放了什么東西、以及都以什么樣的格式去放這些東西。它自最早在 System V 系統(tǒng)上出現(xiàn)后,被 xNIX 世界所廣泛接受,作為缺省的二進(jìn)制文件格式來使用。可以說,ELF是構(gòu)成眾多xNIX系統(tǒng)的基礎(chǔ)之一,所以作為嵌入式Linux系統(tǒng)乃至內(nèi)核驅(qū)動程序開發(fā)人員,你最好熟悉并掌握它。

其實(shí),關(guān)于ELF這個主題,網(wǎng)絡(luò)上已經(jīng)有相當(dāng)多的文章存在,但是其介紹的內(nèi)容比較分散,使得初學(xué)者不太容易從中得到一個系統(tǒng)性的認(rèn)識。為了幫助大家學(xué)習(xí),我這里打算寫一系列連貫的文章來介紹ELF以及相關(guān)的應(yīng)用。這是這個系列中的第一篇文章,主要是通過不同工具的使用來熟悉ELF文件的內(nèi)部結(jié)構(gòu)以及相關(guān)的基本概念。后面的文章,我們會介紹很多高級的概念和應(yīng)用,比方動態(tài)鏈接和加載,動態(tài)庫的開發(fā),C語言Main函數(shù)是被誰以及如何被調(diào)用的,ELF格式在內(nèi)核中的支持,Linux內(nèi)核中對ELF section的擴(kuò)展使用等等。

好的,開始我們的第一篇文章。在詳細(xì)進(jìn)入正題之前,先給大家介紹一點(diǎn)ELF文件格式的參考資料。在ELF格式出來之后,TISC(Tool Interface Standard Committee)委員會定義了一套ELF標(biāo)準(zhǔn)。你可以從這里(http://refspecs.freestandards.org/elf/)找到詳細(xì)的標(biāo)準(zhǔn)文檔。TISC委員會前后出了兩個版本,v1.1和v1.2。兩個版本內(nèi)容上差不多,但就可讀性上來講,我還是推薦你讀 v1.2的。因?yàn)樵趘1.2版本中,TISC重新組織原本在v1.1版本中的內(nèi)容,將它們分成為三個部分(books):

a) Book I

介紹了通用的適用于所有32位架構(gòu)處理器的ELF相關(guān)內(nèi)容

b) Book II

介紹了處理器特定的ELF相關(guān)內(nèi)容,這里是以Intel x86 架構(gòu)處理器作為例子介紹

c) Book III

介紹了操作系統(tǒng)特定的ELF相關(guān)內(nèi)容,這里是以運(yùn)行在x86上面的 UNIX System V.4 作為例子介紹

值得一說的是,雖然TISC是以x86為例子介紹ELF規(guī)范的,但是如果你是想知道非x86下面的ELF實(shí)現(xiàn)情況,那也可以在http://refspecs.freestandards.org/elf/中找到特定處理器相關(guān)的Supplment文檔。比方ARM相關(guān)的,或者M(jìn)IPS相關(guān)的等等。另外,相比較UNIX系統(tǒng)的另外一個分支BSD Unix,Linux系統(tǒng)更靠近 System V 系統(tǒng)。所以關(guān)于操作系統(tǒng)特定的ELF內(nèi)容,你可以直接參考v1.2標(biāo)準(zhǔn)中的內(nèi)容。

這里多說些廢話:別忘了 Linus 在實(shí)現(xiàn)Linux的第一個版本的時候,就是看了介紹Unix內(nèi)部細(xì)節(jié)的書:《The of the Unix Operating System》,得到很多啟發(fā)。這本書對應(yīng)的操作系統(tǒng)是System V 的第二個Release。這本書介紹了操作系統(tǒng)的很多設(shè)計(jì)觀念,并且行文簡單易懂。所以雖然現(xiàn)在的Linux也吸取了其他很多Unix變種的設(shè)計(jì)理念,但是如果你想研究學(xué)習(xí)Linux內(nèi)核,那還是以看這本書作為開始為好。這本書也是我在接觸Linux內(nèi)核之前所看的第一本介紹操作系統(tǒng)的書,所以我極力向大家推薦。(在學(xué)校雖然學(xué)過操作系統(tǒng)原理,但學(xué)的也是很糟糕最后導(dǎo)致期末考試才四十來分,記憶仿佛還在昨天:))

好了,還是回來開始我們第一篇ELF主題相關(guān)的文章吧。這篇文章主要是通過使用不同的工具來分析對象文件,來使你掌握ELF文件的基本格式,以及了解相關(guān)的基本概念。你在讀這篇文章的時候,希望你在電腦上已經(jīng)打開了那個 v1.2 版本的ELF規(guī)范,并對照著文章內(nèi)容看規(guī)范里的文字。

首先,你需要知道的是所謂對象文件(Object files)有三個種類:

1) 可重定位的對象文件(Relocatable file)

這是由匯編器匯編生成的 .o 文件。后面的鏈接器(link editor)拿一個或一些 Relocatable object files 作為輸入,經(jīng)鏈接處理后,生成一個可執(zhí)行的對象文件 (Executable file) 或者一個可被共享的對象文件(Shared object file)。我們可以使用 ar 工具將眾多的 .o Relocatable object files 歸檔(archive)成 .a 靜態(tài)庫文件。如何產(chǎn)生 Relocatable file,你應(yīng)該很熟悉了,請參見我們相關(guān)的基本概念文章和JulWiki。另外,可以預(yù)先告訴大家的是我們的內(nèi)核可加載模塊 .ko 文件也是 Relocatable object file。

2) 可執(zhí)行的對象文件(Executable file)

這我們見的多了。文本編輯器vi、調(diào)式用的工具gdb、播放mp3歌曲的軟件mplayer等等都是Executable object file。你應(yīng)該已經(jīng)知道,在我們的 Linux 系統(tǒng)里面,存在兩種可執(zhí)行的東西。除了這里說的 Executable object file,另外一種就是可執(zhí)行的腳本(如shell腳本)。注意這些腳本不是 Executable object file,它們只是文本文件,但是執(zhí)行這些腳本所用的解釋器就是 Executable object file,比如 bash shell 程序。

3) 可被共享的對象文件(Shared object file)

這些就是所謂的動態(tài)庫文件,也即 .so 文件。如果拿前面的靜態(tài)庫來生成可執(zhí)行程序,那每個生成的可執(zhí)行程序中都會有一份庫代碼的拷貝。如果在磁盤中存儲這些可執(zhí)行程序,那就會占用額外的磁盤空間;另外如果拿它們放到Linux系統(tǒng)上一起運(yùn)行,也會浪費(fèi)掉寶貴的物理內(nèi)存。如果將靜態(tài)庫換成動態(tài)庫,那么這些問題都不會出現(xiàn)。動態(tài)庫在發(fā)揮作用的過程中,必須經(jīng)過兩個步驟:

a) 鏈接編輯器(link editor)拿它和其他Relocatable object file以及其他shared object file作為輸入,經(jīng)鏈接處理后,生存另外的 shared object file 或者 executable file。

b) 在運(yùn)行時,動態(tài)鏈接器(dynamic linker)拿它和一個Executable file以及另外一些 Shared object file 來一起處理,在Linux系統(tǒng)里面創(chuàng)建一個進(jìn)程映像。

以上所提到的 link editor 以及 dynamic linker 是什么東西,你可以參考我們基本概念中的相關(guān)文章。對于什么是編譯器,匯編器等你應(yīng)該也已經(jīng)知道,在這里只是使用他們而不再對他們進(jìn)行詳細(xì)介紹。為了下面的敘述方便,你可以下載test.tar.gz包,解壓縮后使用"make"進(jìn)行編譯。編譯完成后,會在目錄中生成一系列的ELF對象文件,更多描述見里面的 README 文件。我們下面的論述都基于這些產(chǎn)生的對象文件。

make所產(chǎn)生的文件,包括 sub.o/sum.o/test.o/libsub.so/test 等等都是ELF對象文件。至于要知道它們都屬于上面三類中的哪一種,我們可以使用 file 命令來查看:

[yihect@juliantec test]$ file sum.o sub.o test.o libsub.so test 
sum.o:     ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped 
sub.o:     ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped 
test.o:    ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped 
libsub.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), not stripped 
test:      ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), not stripped

結(jié)果很清楚的告訴我們他們都屬于哪一個類別。比方 sum.o 是應(yīng)用在x86架構(gòu)上的可重定位文件。這個結(jié)果也間接的告訴我們,x86是小端模式(LSB)的32位結(jié)構(gòu)。那對于 file 命令來說,它又能如何知道這些信息?答案是在ELF對象文件的最前面有一個ELF文件頭,里面記載了所適用的處理器、對象文件類型等各種信息。在TISCv1.2的規(guī)范中,用下面的圖描述了ELF對象文件的基本組成,其中ELF文件頭赫然在目。

ELF 文件頭

等等,為什么會有左右兩個很類似的圖來說明ELF的組成格式?這是因?yàn)镋LF格式需要使用在兩種場合:

a) 組成不同的可重定位文件,以參與可執(zhí)行文件或者可被共享的對象文件的鏈接構(gòu)建;

b) 組成可執(zhí)行文件或者可被共享的對象文件,以在運(yùn)行時內(nèi)存中進(jìn)程映像的構(gòu)建。

所以,基本上,圖中左邊的部分表示的是可重定位對象文件的格式;而右邊部分表示的則是可執(zhí)行文件以及可被共享的對象文件的格式。正如TISCv1.2規(guī)范中所闡述的那樣,ELF文件頭被固定地放在不同類對象文件的最前面。至于它里面的內(nèi)容,除了file命令所顯示出來的那些之外,更重要的是包含另外一些數(shù)據(jù),用于描述ELF文件中ELF文件頭之外的內(nèi)容。如果你的系統(tǒng)中安裝有 GNU binutils 包,那我們可以使用其中的 readelf 工具來讀出整個ELF文件頭的內(nèi)容,比如:

[yihect@juliantec test]$ readelf -h ./sum.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:          184 (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:         9   Section header string table index: 6  

這個輸出結(jié)果能反映出很多東西。那如何來看這個結(jié)果中的內(nèi)容,我們還是就著TISCv1.2規(guī)范來。在實(shí)際寫代碼支持ELF格式對象文件格式的時候,我們都會定義許多C語言的結(jié)構(gòu)來表示ELF格式的各個相關(guān)內(nèi)容,比方這里的ELF文件頭,你就可以在TISCv1.2規(guī)范中找到這樣的結(jié)構(gòu)定義(注意我們研究的是針對x86架構(gòu)的ELF,所以我們只考慮32位版本,而不考慮其他如64位之類的):

ELF 文件頭結(jié)構(gòu)

這個結(jié)構(gòu)里面出現(xiàn)了多種數(shù)據(jù)類型,同樣可以在規(guī)范中找到相關(guān)說明:

ELF 相關(guān)數(shù)據(jù)類型

在我們以后一系列文章中,我們會著重拿實(shí)際的程序代碼來分析,介時你會在頭文件中找到同樣的定義。但是這里,我們只討論規(guī)范中的定義,暫不考慮任何程序代碼。在ELF頭中,字段e_machine和e_type指明了這是針對x86架構(gòu)的可重定位文件,最前面有個長度為16字節(jié)的字段中有一個字節(jié)表示了它適用于32bits機(jī)器,而不是64位的。除了這些之外,另外ELF頭還告訴了我們其他一些特別重要的信息,分別是:

a) 這個sum.o的進(jìn)入點(diǎn)是0x0(e_entry),這表面Relocatable objects不會有程序進(jìn)入點(diǎn)。所謂程序進(jìn)入點(diǎn)是指當(dāng)程序真正執(zhí)行起來的時候,其第一條要運(yùn)行的指令的運(yùn)行時地址。因?yàn)镽elocatable objects file只是供再鏈接而已,所以它不存在進(jìn)入點(diǎn)。而可執(zhí)行文件test和動態(tài)庫.so都存在所謂的進(jìn)入點(diǎn),你可以用 readelf -h 看看。后面我們的文章中會介紹可執(zhí)行文件的e_entry指向C庫中的_start,而動態(tài)庫.so中的進(jìn)入點(diǎn)指向 call_gmon_start。這些后面再說,這里先不深入討論。

b) 這個sum.o文件包含有9個sections,但卻沒有segments(Number of program headers為0)。

那什么是所謂 sections 呢?可以說,sections 是在ELF文件里頭,用以裝載內(nèi)容數(shù)據(jù)的最小容器。在ELF文件里面,每一個 sections 內(nèi)都裝載了性質(zhì)屬性都一樣的內(nèi)容,比方:

1) .text section 里裝載了可執(zhí)行代碼;

2) .data section 里面裝載了被初始化的數(shù)據(jù);

3) .bss section 里面裝載了未被初始化的數(shù)據(jù);

4) 以 .rec 打頭的 sections 里面裝載了重定位條目;

5) .symtab 或者 .dynsym section 里面裝載了符號信息;

6) .strtab 或者 .dynstr section 里面裝載了字符串信息;

7) 其他還有為滿足不同目的所設(shè)置的section,比方滿足調(diào)試的目的、滿足動態(tài)鏈接與加載的目的等等。

一個ELF文件中到底有哪些具體的 sections,由包含在這個ELF文件中的 section head table(SHT)決定。在SHT中,針對每一個section,都設(shè)置有一個條目,用來描述對應(yīng)的這個section,其內(nèi)容主要包括該 section 的名稱、類型、大小以及在整個ELF文件中的字節(jié)偏移位置等等。我們也可以在TISCv1.2規(guī)范中找到SHT表中條目的C結(jié)構(gòu)定義:

ELF section header entry

我們可以像下面那樣來使用 readelf 工具來查看可重定位對象文件 sum.o 的SHT表內(nèi)容:[yihect@juliantec test]$ readelf -S ./sum.o 
There are 9 section headers, starting at offset 0xb8: 
  
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 00000b 00  AX  0   0  4 
  [ 2] .data             PROGBITS        00000000 000040 000004 00  WA  0   0  4 
  [ 3] .bss              NOBITS          00000000 000044 000000 00  WA  0   0  4 
  [ 4] .note.GNU-stack   PROGBITS        00000000 000044 000000 00      0   0  1 
  [ 5] .comment          PROGBITS        00000000 000044 00002d 00      0   0  1 
  [ 6] .shstrtab         STRTAB          00000000 000071 000045 00      0   0  1 
  [ 7] .symtab           SYMTAB          00000000 000220 0000a0 10      8   7  4 
  [ 8] .strtab           STRTAB          00000000 0002c0 00001d 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)

這個結(jié)果顯示了 sum.o 中包含的所有9個sections。因?yàn)閟um.o僅僅是參與link editor鏈接的可重定位文件,而不參與最后進(jìn)程映像的構(gòu)建,所以Addr(sh_addr)為0。后面你會看到可執(zhí)行文件以及動態(tài)庫文件中大部分sections的這一字段都是有某些取值的。Off(sh_offset)表示了該section離開文件頭部位置的距離。Size(sh_size)表示section的字節(jié)大小。ES(sh_entsize)只對某些形式的sections 有意義。比方符號表 .symtab section,其內(nèi)部包含了一個表格,表格的每一個條目都是特定長度的,那這里的這個字段就表示條目的長度10。Al(sh_addralign)是地址對齊要求。另外剩下的兩列Lk和Inf,對應(yīng)著條目結(jié)構(gòu)中的字段sh_link和字段sh_info。它們中記錄的是section head table 中的條目索引,這就意味著,從這兩個字段出發(fā),可以找到對應(yīng)的另外兩個 section,其具體的含義解釋依據(jù)不同種類的 section 而不同,后面會介紹。

注意上面結(jié)果中的 Flg ,表示的是對應(yīng)section的相關(guān)標(biāo)志。比方.text section 里面存儲的是代碼,所以就是只讀的(X);.data和.bss里面存放的都是可寫的(W)數(shù)據(jù)(非在堆棧中定義的數(shù)據(jù)),只不過前者存的是初始化過的數(shù)據(jù),比方程序中定義的賦過初值的全局變量等;而后者里面存儲的是未經(jīng)過初始化的數(shù)據(jù)。因?yàn)槲唇?jīng)過初始化就意味著不確定這些數(shù)據(jù)剛開始的時候會有些什么樣的值,所以針對對象文件來說,它就沒必要為了存儲這些數(shù)據(jù)而在文件內(nèi)多留出一塊空間,因此.bss section的大小總是為0。后面會看到,當(dāng)可執(zhí)行程序被執(zhí)行的時候,動態(tài)連接器會在內(nèi)存中開辟一定大小的空間來存放這些未初始化的數(shù)據(jù),里面的內(nèi)存單元都被初始化成0。可執(zhí)行程序文件中雖然沒有長度非0的 .bss section,但卻記錄有在程序運(yùn)行時,需要開辟多大的空間來容納這些未初始化的數(shù)據(jù)。

另外一個標(biāo)志A說明對應(yīng)的 section 是Allocable的。所謂 Allocable 的section,是指在運(yùn)行時,進(jìn)程(process)需要使用它們,所以它們被加載器加載到內(nèi)存中去。

而與此相反,存在一些non-Allocable 的sections,它們只是被鏈接器、調(diào)試器或者其他類似工具所使用的,而并非參與進(jìn)程的運(yùn)行中去的那些 section。比方后面要介紹的字符串表section .strtab,符號表 .symtab section等等。當(dāng)運(yùn)行最后的可執(zhí)行程序時,加載器會加載那些 Allocable 的部分,而 non-Allocable 的部分則會被繼續(xù)留在可執(zhí)行文件內(nèi)。所以,實(shí)際上,這些 non-Allocable 的section 都可以被我們用 stip 工具從最后的可執(zhí)行文件中刪除掉,刪除掉這些sections的可執(zhí)行文件照樣能夠運(yùn)行,只不過你沒辦法來進(jìn)行調(diào)試之類的事情罷了。

我們?nèi)匀豢梢允褂?readelf -x SecNum 來傾印出不同 section 中的內(nèi)容。但是,無奈其輸出結(jié)果都是機(jī)器碼,對我們?nèi)藖碚f不具備可讀性。所以我們換用 binutils 包中的另外一個工具 objdump 來看看這些 sections 中到底具有哪些內(nèi)容,先來看看 .text section 的:[yihect@juliantec test]$ objdump -d -j .text ./sum.o 
  
./sum.o:     file format elf32-i386 
  
Disassembly of section .text: 
  
00000000 : 
   0:   55                      push   %ebp 
   1:   89 e5                   mov    %esp,%ebp 
   3:   8b 45 0c                mov    0xc(%ebp),%eax 
   6:   03 45 08                add    0x8(%ebp),%eax 
   9:   c9                      leave  
   a:   c3                      ret

objdump 的選項(xiàng) -d 表示要對由 -j 選擇項(xiàng)指定的 section 內(nèi)容進(jìn)行反匯編,也就是由機(jī)器碼出發(fā),推導(dǎo)出相應(yīng)的匯編指令。上面結(jié)果顯示在 sum.o 對象文件的 .text 中只是包含了函數(shù) sum_func 的定義。用同樣的方法,我們來看看 sum.o 中 .data section 有什么內(nèi)容:[yihect@juliantec test]$ objdump -d -j .data  ./sum.o 
  
./sum.o:     file format elf32-i386 
  
Disassembly of section .data: 
  
00000000 : 
   0:   17 00 00 00                                         ....

這個結(jié)果顯示在 sum.o 的 .data section 中定義了一個四字節(jié)的變量 gv_inited,其值被初始化成 0x00000017,也就是十進(jìn)制值 23。別忘了,x86架構(gòu)是使用小端模式的。

我們接下來來看看字符串表section .strtab。你可以選擇使用 readelf -x :

[yihect@juliantec test]$ readelf -x 8 ./sum.o 
  
Hex dump of section '.strtab': 
  0x00000000 64657469 6e695f76 6700632e 6d757300 .sum.c.gv_inited 
  0x00000010       00 68630063 6e75665f 6d757300 .sum_func.ch.

上面命令中的 8 是 .strtab section 在SHT表格中的索引值,從上面所查看的SHT內(nèi)容中可以找到。盡管這個命令的輸出結(jié)果不是那么具有可讀性,但我們還是得來說一說如何看這個結(jié)果,因?yàn)楹罄m(xù)文章中將會使用大量的這種命令。上面結(jié)果中的十六進(jìn)制數(shù)據(jù)部分從右到左看是地址遞增的方向,而字符內(nèi)容部分從左到右看是地址遞增的方向。所以,在 .strtab section 中,按照地址遞增的方向來看,各字節(jié)的內(nèi)容依次是 0x00、0x73、0x75、0x6d、0x2e ....,也就是字符 、's'、'u'、'm'、'.' ... 等。如果還是看不太明白,你可以使用 hexdump 直接dumping出 .strtab section 開頭(其偏移在文件內(nèi)0x2c0字節(jié)處)的 32 字節(jié)數(shù)據(jù):

[yihect@juliantec test]$ hexdump -s 0x2c0 -n 32 -c ./sum.o 
00002c0     s   u   m   .   c     g   v   _   i   n   i   t   e   d 
00002d0     s   u   m   _   f   u   n   c     c   h              
00002dd

.strtab section 中存儲著的都是以字符 為分割符的字符串,這些字符串所表示的內(nèi)容,通常是程序中定義的函數(shù)名稱、所定義過的變量名稱等等。。。當(dāng)對象文件中其他地方需要和一個這樣的字符串相關(guān)聯(lián)的時候,往往會在對應(yīng)的地方存儲 .strtab section 中的索引值。比方下面將要介紹的符號表 .symtab section 中,有一個條目是用來描述符號 gv_inited 的,那么在該條目中就會有一個字段(st_name)記錄著字符串 gv_inited 在 .strtab section 中的索引 7 。 .shstrtab 也是字符串表,只不過其中存儲的是 section 的名字,而非所函數(shù)或者變量的名稱。

字符串表在真正鏈接和生成進(jìn)程映像過程中是不需要使用的,但是其對我們調(diào)試程序來說就特別有幫助,因?yàn)槲覀內(nèi)丝雌饋碜钍娣倪€是自然形式的字符串,而非像天書一樣的數(shù)字符號。前面使用objdump來反匯編 .text section 的時候,之所以能看到定義了函數(shù) sum_func ,那也是因?yàn)榇嬖谶@個字符串表的原因。當(dāng)然起關(guān)鍵作用的,還是符號表 .symtab section 在其中作為中介,下面我們就來看看符號表。

雖然我們同樣可以使用 readelf -x 來查看符號表(.symtab)section的內(nèi)容,但是其結(jié)果可讀性太差,我們換用 readelf -s 或者 objdump -t 來查看(前者輸出結(jié)果更容易看懂):

[yihect@juliantec test]$ readelf -s ./sum.o 
  
Symbol table '.symtab' contains 10 entries: 
   Num:    Value  Size Type    Bind   Vis      Ndx Name 
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS sum.c 
     2: 00000000     0 SECTION LOCAL  DEFAULT    1 
     3: 00000000     0 SECTION LOCAL  DEFAULT    2 
     4: 00000000     0 SECTION LOCAL  DEFAULT    3 
     5: 00000000     0 SECTION LOCAL  DEFAULT    4 
     6: 00000000     0 SECTION LOCAL  DEFAULT    5 
     7: 00000000     4 OBJECT  GLOBAL DEFAULT    2 gv_inited 
     8: 00000000    11 FUNC    GLOBAL DEFAULT    1 sum_func 
     9: 00000001     1 OBJECT  GLOBAL DEFAULT  COM ch

在符號表內(nèi)針對每一個符號,都會相應(yīng)的設(shè)置一個條目。在繼續(xù)介紹上面的結(jié)果之前,我們還是從規(guī)范中找出符號表內(nèi)條目的C結(jié)構(gòu)定義:

ELF 符號表?xiàng)l目

上面結(jié)果中 Type 列顯示出符號的種類。Bind 列定義了符號的綁定類型。種類和綁定類型合并在一起,由結(jié)構(gòu)中 st_info 字段來定義。在ELF格式中,符號類型總共可以有這么幾種:

ELF 符號類型

類型 STT_OBJECT 表示和該符號對應(yīng)的是一個數(shù)據(jù)對象,比方程序中定義過的變量、數(shù)組等,比方上面的 gv_inited 和 ch;類型 STT_FUNC 表示該符號對應(yīng)的是函數(shù),比方上面的 sum_func函數(shù)。類型 STT_SECTION 表示該符號和一個 section 相關(guān),這種符號用于重定位。關(guān)于重定位,我們下文會介紹。

符號的綁定類型表示了這個符號的可見性,是僅本對象文件可見呢,還是全局可見。它的取值主要有三種:STB_LOCA、STB_GLOBAL和STB_WEAK,具體的內(nèi)容還請參見規(guī)范。關(guān)于符號,最重要的就是符號的值(st_value)了。依據(jù)對象文件的不同類型,符號的值所表示的含義也略有差異:

a) 在可重定位文件中,如果該符號對應(yīng)的section index(上面的Ndx)為SHN_COMMON,那么符號的值表示的是該數(shù)據(jù)的對齊要求,比方上面的變量 ch 。

b) 在可重定位文件中,除去上面那條a中定義的符號,對于其他的符號來說,其值表示的是對應(yīng) section 內(nèi)的偏移值。比方 gv_inited 變量定義在 .data section 的最前面,所以其值為0。

c) 在可執(zhí)行文件或者動態(tài)庫中,符號的值表示的是運(yùn)行時的內(nèi)存地址。

好,咱們再來介紹重定位。在所產(chǎn)生的對象文件 test.o 中有對函數(shù) sum_func 的引用,這對我們的x386結(jié)構(gòu)來說,其實(shí)就是一條call指令。既然 sum_func 是定義在 sum.o 中的,那對 test.o 來說,它就是一個外部引用。所以,匯編器在產(chǎn)生 test.o 的時候,它會產(chǎn)生一個重定位條目。重定位條目中會包含以下幾類東西:

1) 它會包含一個符號表中一個條目的索引,因?yàn)檫@樣我們才知道它具體是哪個符號需要被重定位的;

2) 它會包含一個 .text section 中的地址單元的偏移值。原本這個偏移值處的地址單元里面應(yīng)該存放著 call 指令的操作數(shù)。對上面來說,也就是函數(shù) sum_func 的地址,但是目前這個地址匯編器還不知道。

3) 它還會包含一個tag,以指明該重定位屬于何種類型。

當(dāng)我們用鏈接器去鏈接這個對象文件的時候,鏈接器會遍歷所有的重定位條目,碰到像 sum_func 這樣的外部引用,它會找到 sum_func 的確切地址,并且把它寫回到上面 call 指令操作數(shù)所占用的那個地址單元。像這樣的操作,稱之為重定位操作。link editor 和 dynamic linker 都要完成一些重定位操作,只不過后者的動作更加復(fù)雜,因?yàn)樗窃谶\(yùn)行時動態(tài)完成的,我們以后的文章會介紹相關(guān)的內(nèi)容。概括一下,所謂重定位操作就是:“匯編的時候產(chǎn)生一個空坐位,上面用紅紙寫著要坐在這個座位上的人的名字,然后連接器在開會前安排那個人坐上去”。

如前面我們說過的,對象文件中的重定位條目,會構(gòu)成一個個單獨(dú)的 section。這些 section 的名字,常會是這樣的形式:".rel.XXX"。其中XXX表示的是這些重定位條目所作用到的section,如 .text section。重定位條目所構(gòu)成的section需要和另外兩個section產(chǎn)生關(guān)聯(lián):符號表section(表示要重定位的是哪一個符號)以及受影響地址單元所在的section。在使用工具來查看重定位section之前,我們先從規(guī)范中找出來表示重定位條目的結(jié)構(gòu)定義(有兩種,依處理器架構(gòu)來定):

ELF 重定位條目結(jié)構(gòu)定義

結(jié)構(gòu)中 r_offset 對于可重定位文件.o來說,就是地址單元的偏移值(前面的b條);另外對可執(zhí)行文件或者動態(tài)庫來說,就是該地址單元的運(yùn)行時地址。上面 a條中的符號表內(nèi)索引和c條中的類型,一起構(gòu)成了結(jié)構(gòu)中的字段 r_info。

重定位過程在計(jì)算最終要放到受影響地址單元中的時候,需要加上一個附加的數(shù) addend。當(dāng)某一種處理器選用 Elf32_Rela 結(jié)構(gòu)的時候,該 addend 就是結(jié)構(gòu)中的 r_addend 字段;否則該 addend 就是原本存儲在受影響地址單元中的原有值。x86架構(gòu)選用 Elf32_Rel 結(jié)構(gòu)來表示重定位條目。ARM架構(gòu)也是用這個。

重定位類型意味著如何去修改受影響的地址單元,也就是按照何種方式去計(jì)算需要最后放在受影響單元里面的值。具體的重定位類型有哪些,取決與特定的處理器架構(gòu),你可以參考相關(guān)規(guī)范。這種計(jì)算方式可以非常的簡單,比如在x386上的 R_386_32 類型,它規(guī)定只是將附加數(shù)加上符號的值作為所需要的值;該計(jì)算方式也可以是非常的復(fù)雜,比如老版本ARM平臺上的 R_ARM_PC26。在這篇文章的末尾,我會詳細(xì)介紹一種重定位類型:R_386_PC32。至于另外一些重要的重定位類型,如R_386_GOTPC,R_386_PLT32,R_386_GOT32,R_386_GLOB_DAT 以及 R_386_JUMP_SLOT 等。讀者可以先自己研究,也許我們會在后面后面的文章中討論到相關(guān)主題時再行介紹。

我們可以使用命令 readelf -r 來查看重定位信息:

[yihect@juliantec test_2]$ readelf -r test.o 
  
Relocation section '.rel.text' at offset 0x464 contains 8 entries: 
Offset     Info    Type            Sym.Value  Sym. Name 
00000042  00000902 R_386_PC32        00000000   sub_func 
00000054  00000a02 R_386_PC32        00000000   sum_func 
0000005d  00000a02 R_386_PC32        00000000   sum_func 
0000007a  00000501 R_386_32          00000000   .rodata 
0000007f  00000b02 R_386_PC32        00000000   printf 
0000008d  00000c02 R_386_PC32        00000000   double_gv_inited 
00000096  00000501 R_386_32          00000000   .rodata 
0000009b  00000b02 R_386_PC32        00000000   printf

至此,ELF對象文件格式中的 linking view ,也就是上面組成圖的左邊部分,我們已經(jīng)介紹完畢。在這里最重要的概念是 section。在可重定位文件里面,section承載了大多數(shù)被包含的東西,代碼、數(shù)據(jù)、符號信息、重定位信息等等。可重定位對象文件里面的這些sections是作為輸入,給鏈接器那去做鏈接用的,所以這些 sections 也經(jīng)常被稱做輸入 section。

鏈接器在鏈接可執(zhí)行文件或動態(tài)庫的過程中,它會把來自不同可重定位對象文件中的相同名稱的 section 合并起來構(gòu)成同名的 section。接著,它又會把帶有相同屬性(比方都是只讀并可加載的)的 section 都合并成所謂 segments(段)。segments 作為鏈接器的輸出,常被稱為輸出section。我們開發(fā)者可以控制哪些不同.o文件的sections來最后合并構(gòu)成不同名稱的 segments。如何控制呢,就是通過 linker script 來指定。關(guān)于鏈接器腳本,我們這里不予討論。

一個單獨(dú)的 segment 通常會包含幾個不同的 sections,比方一個可被加載的、只讀的segment 通常就會包括可執(zhí)行代碼section .text、只讀的數(shù)據(jù)section .rodata以及給動態(tài)鏈接器使用的符號section .dymsym等等。section 是被鏈接器使用的,但是 segments 是被加載器所使用的。加載器會將所需要的 segment 加載到內(nèi)存空間中運(yùn)行。和用 sections header table 來指定一個可重定位文件中到底有哪些 sections 一樣。在一個可執(zhí)行文件或者動態(tài)庫中,也需要有一種信息結(jié)構(gòu)來指出包含有哪些 segments。這種信息結(jié)構(gòu)就是 program header table,如ELF對象文件格式中右邊的 execute view 所示的那樣。

我們可以用 readelf -l 來查看可執(zhí)行文件的程序頭表,如下所示:

[yihect@juliantec test_2]$ readelf -l ./test 
  
Elf file type is EXEC (Executable file) 
Entry point 0x8048464 
There are 7 program headers, starting at offset 52 
  
Program Headers: 
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align 
  PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4 
  INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1 
      [Requesting program interpreter: /lib/ld-linux.so.2] 
  LOAD           0x000000 0x08048000 0x08048000 0x0073c 0x0073c R E 0x1000 
  LOAD           0x00073c 0x0804973c 0x0804973c 0x00110 0x00118 RW  0x1000 
  DYNAMIC        0x000750 0x08049750 0x08049750 0x000d0 0x000d0 RW  0x4 
  NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4 
  GNU_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

結(jié)果顯示,在可執(zhí)行文件 ./test 中,總共有7個 segments。同時,該結(jié)果也很明白顯示出了哪些 section 映射到哪一個 segment 當(dāng)中去。比方在索引為2的那個segment 中,總共有15個 sections 映射進(jìn)來,其中包括我們前面提到過的 .text section。注意這個segment 有兩個標(biāo)志: R 和 E。這個表示該segment是可讀的,也可執(zhí)行的。如果你看到標(biāo)志中有W,那表示該segment是可寫的。

我們還是來解釋一下上面的結(jié)果,希望你能對照著TISCv1.2規(guī)范里面的文本來看,我這里也列出程序頭表?xiàng)l目的C結(jié)構(gòu):

ELF 程序頭表項(xiàng)

上面類型為PHDR的segment,用來包含程序頭表本身。類型為INTERP的segment只包含一個 section,那就是 .interp。在這個section中,包含了動態(tài)鏈接過程中所使用的解釋器路徑和名稱。在Linux里面,這個解釋器實(shí)際上就是 /lib/ ,這可以通過下面的 hexdump 看出來:[yihect@juliantec test_2]$ hexdump -s 0x114 -n 32 -C  ./test  
00000114  2f 6c 69 62 2f 6c 64 2d  6c 69 6e 75 78 2e 73 6f  |/lib/ld-linux.so| 
00000124  2e 32 00 00 04 00 00 00  10 00 00 00 01 00 00 00  |.2..............| 
00000134

為什么會有這樣的一個 segment?這是因?yàn)槲覀儗懙膽?yīng)用程序通常都需要使用動態(tài)鏈接庫.so,就像 test 程序中所使用的 libsub.so 一樣。我們還是先大致說說程序在linux里面是怎么樣運(yùn)行起來的吧。當(dāng)你在 shell 中敲入一個命令要執(zhí)行時,內(nèi)核會幫我們創(chuàng)建一個新的進(jìn)程,它在往這個新進(jìn)程的進(jìn)程空間里面加載進(jìn)可執(zhí)行程序的代碼段和數(shù)據(jù)段后,也會加載進(jìn)動態(tài)連接器(在Linux里面通常就是 /lib/ld-linux.so 符號鏈接所指向的那個程序,它本省就是一個動態(tài)庫)的代碼段和數(shù)據(jù)。在這之后,內(nèi)核將控制傳遞給動態(tài)鏈接庫里面的代碼。動態(tài)連接器接下來負(fù)責(zé)加載該命令應(yīng)用程序所需要使用的各種動態(tài)庫。加載完畢,動態(tài)連接器才將控制傳遞給應(yīng)用程序的main函數(shù)。如此,你的應(yīng)用程序才得以運(yùn)行。

這里說的只是大致的應(yīng)用程序啟動運(yùn)行過程,更詳細(xì)的,我們會在后續(xù)的文章中繼續(xù)討論。我們說link editor鏈接的應(yīng)用程序只是部分鏈接過的應(yīng)用程序。經(jīng)常的,在應(yīng)用程序中,會使用很多定義在動態(tài)庫中的函數(shù)。最最基礎(chǔ)的比方C函數(shù)庫(其本身就是一個動態(tài)庫)中定義的函數(shù),每個應(yīng)用程序總要使用到,就像我們test程序中使用到的 printf 函數(shù)。為了使得應(yīng)用程序能夠正確使用動態(tài)庫,動態(tài)連接器在加載動態(tài)庫后,它還會做更進(jìn)一步的鏈接,這就是所謂的動態(tài)鏈接。為了讓動態(tài)連接器能成功的完成動態(tài)鏈接過程,在前面運(yùn)行的link editor需要在應(yīng)用程序可執(zhí)行文件中生成數(shù)個特殊的 sections,比方 .dynamic、.dynsym、.got和.plt等等。這些內(nèi)容我們會在后面的文章中進(jìn)行討論。

我們先回到上面所輸出的文件頭表中。在接下來的數(shù)個 segments 中,最重要的是三個 segment:代碼段,數(shù)據(jù)段和堆棧段。代碼段和堆棧段的 VirtAddr 列的值分別為 0x08048000 和 0x0804973c。這是什么意思呢?這是說對應(yīng)的段要加載在進(jìn)程虛擬地址空間中的起始地址。雖然在可執(zhí)行文件中規(guī)定了 text segment和 data segment 的起始地址,但是最終,在內(nèi)存中的這些段的真正起始地址,卻可能不是這樣的,因?yàn)樵趧討B(tài)鏈接器加載這些段的時候,需要考慮到頁面對齊的因素。為什么?因?yàn)橄駒86這樣的架構(gòu),它給內(nèi)存單元分配讀寫權(quán)限的最小單位是頁(page)而不是字節(jié)。也就是說,它能規(guī)定從某個頁開始、連續(xù)多少頁是只讀的。卻不能規(guī)定從某個頁內(nèi)的哪一個字節(jié)開始,連續(xù)多少個字節(jié)是只讀的。因?yàn)閤86架構(gòu)中,一個page大小是4k,所以,動態(tài)鏈接器在加載 segment 到虛擬內(nèi)存中的時候,其真實(shí)的起始地址的低12位都是零,也即以 0x1000 對齊。

我們先來看看一個真實(shí)的進(jìn)程中的內(nèi)存空間信息,拿我們的 test 程序作為例子。在 Linux 系統(tǒng)中,有一個特殊的由內(nèi)核實(shí)現(xiàn)的虛擬文件系統(tǒng) /proc。內(nèi)核實(shí)現(xiàn)這個文件系統(tǒng),并將它作為整個Linux系統(tǒng)面向外部世界的一個接口。我們可以通過 /proc 觀察到一個正在運(yùn)行著的Linux系統(tǒng)的內(nèi)核數(shù)據(jù)信息以及各進(jìn)程相關(guān)的信息。所以我們?nèi)绻榭茨骋粋€進(jìn)程的內(nèi)存空間情況,也可以通過它來進(jìn)行。使用/proc唯一需要注意的是,由于我們的 test 程序很小,所以當(dāng)我們運(yùn)行起來之后,它很快就會結(jié)束掉,使得我們沒有時間去查看test的進(jìn)程信息。我們需要想辦法讓它繼續(xù)運(yùn)行,或者最起碼運(yùn)行直到讓我們能從 /proc 中獲取得到想要的信息后再結(jié)束。

我們有多種選擇。最簡單的是,在 test main 程序中插入一個循環(huán),然后在循環(huán)中放入 sleep() 的調(diào)用,這樣當(dāng)程序運(yùn)行到這個循環(huán)的時候,就會進(jìn)入“運(yùn)行-睡眠-運(yùn)行-睡眠”循環(huán)中。這樣我們就有機(jī)會去看它的虛擬內(nèi)存空間信息。另外一個方法,是使用調(diào)試器,如GDB。我們設(shè)置一個斷點(diǎn),然后在調(diào)試過程中讓test進(jìn)程在這個斷點(diǎn)處暫停,這樣我們也有機(jī)會獲得地址空間的信息。我們這里就使用這種方法。當(dāng)然,為了能讓GDB調(diào)試我們的 test,我們得在編譯的時候加上"-g"選項(xiàng)。最后我們用下面的命令得到 test 程序?qū)?yīng)進(jìn)程的地址空間信息。

[yihect@juliantec ~]$ cat /proc/`pgrep test`/maps 
00103000-00118000 r-xp 00000000 08:02 544337     /lib/ld-2.3.4.so 
00118000-00119000 r--p 00015000 08:02 544337     /lib/ld-2.3.4.so 
00119000-0011a000 rw-p 00016000 08:02 544337     /lib/ld-2.3.4.so 
0011c000-00240000 r-xp 00000000 08:02 544338     /lib/tls/libc-2.3.4.so 
00240000-00241000 r--p 00124000 08:02 544338     /lib/tls/libc-2.3.4.so 
00241000-00244000 rw-p 00125000 08:02 544338     /lib/tls/libc-2.3.4.so 
00244000-00246000 rw-p 00244000 00:00 0 
00b50000-00b51000 r-xp 00000000 08:02 341824     /usr/lib/libsub.so 
00b51000-00b52000 rw-p 00000000 08:02 341824     /usr/lib/libsub.so 
08048000-08049000 r-xp 00000000 08:05 225162     /home/yihect/test_2/test 
08049000-0804a000 rw-p 00000000 08:05 225162     /home/yihect/test_2/test 
b7feb000-b7fed000 rw-p b7feb000 00:00 0 
b7fff000-b8000000 rw-p b7fff000 00:00 0 
bff4c000-c0000000 rw-p bff4c000 00:00 0 
ffffe000-fffff000 ---p 00000000 00:00 0

注意,上面命令中的pgre test 是用`括起來的,它不是單引號,而是鍵盤上 Esc 字符下面的那個字符。從這個結(jié)果上可以看出,所有的段,其起始地址和結(jié)束地址(前面兩列)都是0x1000對齊的。結(jié)果中也列出了對應(yīng)的段是從哪里引過來的,比方動態(tài)鏈接器/lib/ld-2.3.4.so、C函數(shù)庫和test程序本身。注意看test程序引入的代碼段起始地址是 0x08048000,這和我們 ELF 文件中指定的相同,但是結(jié)束地址卻是0x08049000,和文件中指定的不一致(0x08048000+0x0073c=0x0804873c)。這里,其實(shí)加載器也把數(shù)據(jù)segment中開頭一部分也映射進(jìn)了 text segment 中去;同樣的,進(jìn)程虛擬內(nèi)存空間中的 data segment 從 08049000 開始,而可執(zhí)行文件中指定的是從 0x0804973c 開始。所以加載器也把代碼segment中末尾一部分也映射進(jìn)了 data segment 中去了。

從程序頭表中我們可以看到一個類型為 GNU_STACK 的segment,這是 stack segment。程序頭表中的這一項(xiàng),除了 Flg/Align 兩列不為空外, 其他列都為0。這是因?yàn)槎褩6卧谔摂M內(nèi)存空間中,從哪里開始、占多少字節(jié)是由內(nèi)核說了算的,而不決定于可執(zhí)行程序。實(shí)際上,內(nèi)核決定把堆棧段放在整個進(jìn)程地址空間的用戶空間的最上面,所以堆棧段的末尾地址就是 0xc0000000。別忘記在 x86 中,堆棧是從高向低生長的。

好,為了方便你對后續(xù)文章的理解,我們在這里討論一種比較簡單的重定位類型 R_386_PC32。前面我們說過重定義的含義,也即在連接階段,根據(jù)某種計(jì)算方式計(jì)算出一個新的值(通常是地址),然后將這個值重新改寫到對象文件或者內(nèi)存映像中某個section中的某個地址單元中去的這樣一個過程。那所謂重定位類型,就規(guī)定了使用何種方式,去計(jì)算這個值。既然是計(jì)算,那就肯定需要涉及到所要納入計(jì)算的變量。實(shí)際上,具體有哪些變量參與計(jì)算如同如何進(jìn)行計(jì)算一樣也是不固定的,各種重定位類型有自己的規(guī)定。

根據(jù)規(guī)范里面的規(guī)定,重定位類型 R_386_PC32 的計(jì)算需要有三個變量參與:S,A和P。其計(jì)算方式是 S+A-P。根據(jù)規(guī)范,當(dāng)R_386_PC32類型的重定位發(fā)生在 link editor 鏈接若干個 .o 對象文件從而形成可執(zhí)行文件的過程中的時候,變量S指代的是被重定位的符號的實(shí)際運(yùn)行時地址,而變量P是重定位所影響到的地址單元的實(shí)際運(yùn)行時地址。在運(yùn)行于x86架構(gòu)上的Linux系統(tǒng)中,這兩個地址都是虛擬地址。變量A最簡單,就是重定位所需要的附加數(shù),它是一個常數(shù)。別忘了x86架構(gòu)所使用的重定位條目結(jié)構(gòu)體類型是 Elf32_Rela,所以附加數(shù)就存在于受重定位影響的地址單元中。重定位最后將計(jì)算得到的值patch到這個地址單元中。

或許,咱們舉一個實(shí)際例子來闡述可能對你更有用。在我們的 test 程序中,test.c 的 main 函數(shù)中需要調(diào)用定義在 sum.o 中的 sum_func 函數(shù),所以link editor 在將 test.o/sum.o 聯(lián)結(jié)成可執(zhí)行文件 test 的時候,必須處理一個重定位,這個重定位就是 R_386_PC32 類型的。我們先用 objdump 來查看 test.o 中的 .text section 內(nèi)容(我只選取了前面一部分):[yihect@juliantec test_2]$ objdump -d -j .text ./test.o 
  
./test.o:     file format elf32-i386 
  
Disassembly of section .text: 
  
00000000 <main />: 
   0:   55                      push   %ebp 
   1:   89 e5                   mov    %esp,%ebp 
   3:   83 ec 18                sub    $0x18,%esp 
   6:   83 e4 f0                and    $0xfffffff0,%esp 
   9:   b8 00 00 00 00          mov    $0x0,%eax 
   e:   83 c0 0f                add    $0xf,%eax 
  11:   83 c0 0f                add    $0xf,%eax 
  14:   c1 e8 04                shr    $0x4,%eax 
  17:   c1 e0 04                shl    $0x4,%eax 
  1a:   29 c4                   sub    %eax,%esp 
  1c:   c7 45 fc 0a 00 00 00    movl   $0xa,0xfffffffc(%ebp) 
  23:   c7 45 f8 2d 00 00 00    movl   $0x2d,0xfffffff8(%ebp) 
  2a:   c7 45 f4 03 00 00 00    movl   $0x3,0xfffffff4(%ebp) 
  31:   c7 45 f0 48 00 00 00    movl   $0x48,0xfffffff0(%ebp) 
  38:   83 ec 08                sub    $0x8,%esp 
  3b:   ff 75 f0                pushl  0xfffffff0(%ebp) 
  3e:   ff 75 f4                pushl  0xfffffff4(%ebp) 
  41:   e8 fc ff ff ff          call   42 
  46:   83 c4 08                add    $0x8,%esp 
  49:   50                      push   %eax 
  4a:   83 ec 0c                sub    $0xc,%esp 
  4d:   ff 75 f8                pushl  0xfffffff8(%ebp) 
  50:   ff 75 fc                pushl  0xfffffffc(%ebp) 
  53:   e8 fc ff ff ff          call   54 
  58:   83 c4 14                add    $0x14,%esp 
  ......

如結(jié)果所示,在離開 .text section 開始 0x53 字節(jié)的地方,有一條call指令。這條指令是對 sum_func 函數(shù)的調(diào)用,objdump 將其反匯編成 call 54,這是因?yàn)槠?0x54 字節(jié)的地方原本應(yīng)該放著 sum_func 函數(shù)的地址,但現(xiàn)在因?yàn)?sum_func 定義在 sum.o 中,所以這個地方就是重定位需要做 patch 的地址單元所在處。我們注意到,這個地址單元的值為 0xfffffffc,也就是十進(jìn)制的 -4(計(jì)算機(jī)中數(shù)是用補(bǔ)碼表示的)。所以,參與重定位運(yùn)算的變量A就確定了,即是 -4。

我們在 test.o 中找出影響該地址單元的重定位記錄如下:

[yihect@juliantec test_2]$ readelf -r ./test.o |  grep 54 
00000054  00000a02 R_386_PC32        00000000   sum_func

果然,如你所見,該條重定位記錄是 R_386_PC32 類型的。前面變量A確定了,那么另外兩個變量S和變量P呢?從正向去計(jì)算這兩個變量的值比較麻煩。盡管我們知道,在Linux里面,鏈接可執(zhí)行程序時所使用的默認(rèn)的鏈接器腳本將最后可執(zhí)行程序的 .text segment 起始地址設(shè)置在 0x08048000的位置。但是,從這個地址出發(fā),去尋找符號(函數(shù))sub_func 和 上面受重定位影響的地址單元的運(yùn)行時地址的話,需要經(jīng)過很多人工計(jì)算,所以比較麻煩。

相反的,我們使用objdump工具像下面這樣分析最終鏈接生成的可執(zhí)行程序 ./test 的 .text segment 段,看看函數(shù) sum_func 和 那個受影響單元的運(yùn)行時地址到底是多少,這是反向的查看鏈接器的鏈接結(jié)果。鏈接器在鏈接的過程中是正向的將正確的地址分配給它們的。

[yihect@juliantec test_2]$ objdump -d -j .text ./test 
  
./test:     file format elf32-i386 
  
Disassembly of section .text: 
  
08048498 : 
8048498:       31 ed                   xor    %ebp,%ebp 
...... 
08048540 <main />: 
...... 
804858a:       83 ec 0c                sub    $0xc,%esp 
804858d:       ff 75 f8                pushl  0xfffffff8(%ebp) 
8048590:       ff 75 fc                pushl  0xfffffffc(%ebp) 
8048593:       e8 74 00 00 00          call   804860c 
8048598:       83 c4 14                add    $0x14,%esp 
804859b:       50                      push   %eax 
...... 

0804860c : 
804860c:       55                      push   %ebp 
804860d:       89 e5                   mov    %esp,%ebp 
804860f:       8b 45 0c                mov    0xc(%ebp),%eax 
8048612:       03 45 08                add    0x8(%ebp),% 
8048615:       c9                      leave  
8048616:       c3                      ret    
8048617:       90                      nop 
  
......

從中很容易的就可以看出,鏈接器給函數(shù) sum_func 分配的運(yùn)行時地址是 0x0804860c,所以變量S的值就是 0x0804860c。那么變量P呢?它表示的是重定位所影響地址單元的運(yùn)行地址。如果要計(jì)算這個地址,我們可以先看看 main 函數(shù)的運(yùn)行時地址,再加上0x54字節(jié)的偏移來得到。從上面看出 main 函數(shù)的運(yùn)行時地址為 0x08048540,所以重定位所影響地址單元的運(yùn)行時地址為 0x08048540+0x54 = 0x08048594。所以重定位計(jì)算的最終結(jié)果為:

S+A-P = 0x0804860c+(-4)-0x08048594 = 0x00000074

從上面可以看出,鏈接器在鏈接過程中,確實(shí)也把這個計(jì)算得到的結(jié)果存儲到了上面 call 指令操作數(shù)所在的地址單元中去了。那么,程序在運(yùn)行時,是如何憑借這樣一條帶有如此操作數(shù)的 call 指令來調(diào)用到(或者跳轉(zhuǎn)到)函數(shù) sum_func 中去的呢?

你看,調(diào)用者 main 和被調(diào)用者 sum_func 處在同一個text segment中。根據(jù)x86架構(gòu)或者IBM兼容機(jī)的匯編習(xí)慣,段內(nèi)轉(zhuǎn)移或者段內(nèi)跳轉(zhuǎn)時使用的尋址方式是PC相對尋址。也就是若要讓程序從一個段內(nèi)的A處,跳轉(zhuǎn)到同一段內(nèi)的B處,那么PC相對尋址會取程序在A處執(zhí)行時的PC值,再加上某一個偏移值(offset),得到要跳轉(zhuǎn)的目標(biāo)地址(B處地址)。那么,對于x86架構(gòu)來說,由于有規(guī)定,PC總是指向下一條要執(zhí)行的指令,那么當(dāng)程序執(zhí)行在call指令的時候,PC指向的是下一條add指令,其值也就是 0x8048598。最后,尋址的時候再加上call指令的操作數(shù)0x74作為偏移,計(jì)算最終的 sum_func 函數(shù)目標(biāo)地址為 0x8048598+0x74 = 0x804860c。

有點(diǎn)意思吧:),如果能繞出來,那說明我們是真的明白了,其實(shí),繞的過程本身就充滿著趣味性,就看你自己的心態(tài)了。說到這里,本文行將結(jié)束。本文所介紹的很多內(nèi)容,可能在某些同學(xué)眼中會過于簡單,但是為了體現(xiàn)知識的完整性、同時也為了讓大家先有個基礎(chǔ)以便更容易的看后續(xù)的文章,我們還是在這里介紹一下ELF格式的基礎(chǔ)知識。下面一篇關(guān)于ELF主題的文章,將詳細(xì)介紹動態(tài)連接的內(nèi)在實(shí)現(xiàn)。屆時,你將看到大量的實(shí)際代碼挖掘。

posted on 2012-12-07 22:59 tqsheng 閱讀(271) 評論(0)  編輯 收藏 引用


只有注冊用戶登錄后才能發(fā)表評論。
網(wǎng)站導(dǎo)航: 博客園   IT新聞   BlogJava   博問   Chat2DB   管理


青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            亚洲一区二区三区四区五区黄| 亚洲国产成人高清精品| 香蕉久久国产| 国产精品mm| 亚洲综合精品自拍| 久久精品成人| 一区二区在线观看av| 香蕉尹人综合在线观看| 久久久999精品视频| 黑人一区二区| 欧美成人午夜影院| 夜夜嗨网站十八久久| 亚洲欧美综合| 精品电影在线观看| 欧美人与性禽动交情品| 亚洲一区在线播放| 久久精品亚洲一区| 亚洲精品一区中文| 国产老肥熟一区二区三区| 欧美在线一级视频| 亚洲国产欧美久久| 一本在线高清不卡dvd| 国产精品久久97| 久久资源在线| 亚洲夜晚福利在线观看| 免费观看久久久4p| 亚洲欧美日韩人成在线播放| 在线免费精品视频| 国产精品第一区| 免费短视频成人日韩| 亚洲尤物影院| 欧美激情第六页| 欧美一区二区三区的| 亚洲精品婷婷| 久久久成人精品| 亚洲视频1区2区| 亚洲国产黄色| 国产在线观看精品一区二区三区| 欧美日韩精品免费在线观看视频| 久久国产乱子精品免费女| 亚洲深夜福利网站| 亚洲精品久久久久久久久| 蜜桃av一区| 久久久蜜桃一区二区人| 午夜精品久久久久久99热软件| 亚洲精品日韩激情在线电影| 激情亚洲网站| 国产一区二区日韩| 国产伦精品一区二区三区免费迷| 欧美日韩免费观看一区三区 | 久久婷婷国产综合国色天香| 一二美女精品欧洲| 亚洲精选久久| 亚洲精品一级| 亚洲电影在线免费观看| 久久久之久亚州精品露出| 亚洲一区在线观看视频| 亚洲国产成人久久综合| 狠狠入ady亚洲精品经典电影| 欧美性生交xxxxx久久久| 欧美综合二区| 亚洲图片欧美一区| 国产精品99久久久久久久久| 日韩图片一区| 日韩一级裸体免费视频| 亚洲日本中文| 一区二区三区av| 中日韩高清电影网| 亚洲综合色自拍一区| 亚洲欧美国产另类| 欧美一区免费| 久久精品动漫| 久久一综合视频| 欧美成人午夜免费视在线看片| 久久精品一区| 亚洲国产专区| 亚洲一级在线| 久久久精品国产免大香伊| 久久夜色精品国产噜噜av| 美女国内精品自产拍在线播放| 你懂的视频欧美| 欧美另类综合| 国产精品wwwwww| 国产亚洲精品久久久久久| 国产综合自拍| 亚洲日本在线视频观看| 亚洲一区二区3| 香港久久久电影| 免费h精品视频在线播放| 欧美国产日本高清在线| 亚洲美女在线视频| 欧美主播一区二区三区| 欧美电影电视剧在线观看| 免费人成网站在线观看欧美高清 | 亚洲欧美日韩精品| 欧美在线视频不卡| 欧美福利影院| 99视频有精品| 久久久久久精| 欧美视频在线观看视频极品| 国产精品欧美一区喷水 | 亚洲二区免费| 麻豆精品在线播放| 欧美特黄一级| 国产精品久久久久久久电影| 极品尤物久久久av免费看| 一区二区三区四区五区视频| 欧美在线高清| 亚洲美女在线观看| 久久伊伊香蕉| 欧美精品一区二区在线观看| 国产欧美精品一区二区三区介绍| 亚洲第一综合天堂另类专| 在线视频欧美一区| 免费在线日韩av| 午夜精品久久久久| 欧美日韩在线大尺度| 在线观看一区二区视频| 欧美与黑人午夜性猛交久久久| 欧美激情欧美激情在线五月| 午夜久久美女| 欧美三级网页| 一本色道久久综合亚洲91| 欧美一区激情视频在线观看| 亚洲精品一二三区| 欧美激情一区二区| 亚洲国产精品久久久久婷婷884 | 久久精品男女| 99在线视频精品| 欧美激情第10页| 在线观看国产成人av片| 久久精品亚洲国产奇米99| 一区二区国产精品| 欧美日韩大片| 亚洲少妇最新在线视频| 亚洲精品视频一区| 男女视频一区二区| 在线欧美视频| 欧美成人伊人久久综合网| 久久国产夜色精品鲁鲁99| 国产一区二区三区在线观看免费视频 | 国产精品试看| 午夜精品久久久久久久久| 一区二区免费在线观看| 欧美日韩国产限制| 一二三四社区欧美黄| 亚洲人成在线免费观看| 欧美另类一区| 亚洲一级影院| 亚洲图片激情小说| 国产精品久久久久国产a级| 在线视频欧美日韩| 亚洲午夜一级| 国产亚洲一区二区三区| 久久综合五月天婷婷伊人| 久久精品在线观看| 激情小说亚洲一区| 亚洲国产综合在线看不卡| 欧美理论电影在线观看| 一本色道久久综合狠狠躁篇怎么玩| 99re在线精品| 国产美女搞久久| 老司机免费视频一区二区| 欧美成人亚洲| 欧美一级一区| 久久久久国产一区二区三区四区| 乱码第一页成人| 一区二区精品| 午夜日韩电影| 亚洲全部视频| 亚洲欧美另类综合偷拍| 在线成人免费视频| 亚洲精品在线视频| 欧美视频导航| aⅴ色国产欧美| 亚洲欧美日韩一区在线观看| 欧美激情一区二区三区| 欧美一区二区三区视频免费| 亚洲在线免费| 欧美日韩综合在线免费观看| 久久成人羞羞网站| 欧美高清在线视频观看不卡| 亚洲欧美久久久| 免费观看30秒视频久久| 久久爱www久久做| 免费在线看一区| 久久都是精品| 欧美日韩国产一级| 免费一级欧美在线大片| 欧美制服丝袜第一页| 亚洲人成亚洲人成在线观看| 夜夜嗨av一区二区三区免费区| 欧美精品免费播放| 99热在这里有精品免费| 久久精品免费观看| 亚洲欧美日韩区| 麻豆精品在线观看| 亚洲一区二区三区三| 久久亚洲精品一区| 亚洲女同在线|