• <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>

            tqsheng

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

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

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

            其實,關(guān)于ELF這個主題,網(wǎng)絡(luò)上已經(jīng)有相當多的文章存在,但是其介紹的內(nèi)容比較分散,使得初學(xué)者不太容易從中得到一個系統(tǒng)性的認識。為了幫助大家學(xué)習,我這里打算寫一系列連貫的文章來介紹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的擴展使用等等。

            好的,開始我們的第一篇文章。在詳細進入正題之前,先給大家介紹一點ELF文件格式的參考資料。在ELF格式出來之后,TISC(Tool Interface Standard Committee)委員會定義了一套ELF標準。你可以從這里(http://refspecs.freestandards.org/elf/)找到詳細的標準文檔。TISC委員會前后出了兩個版本,v1.1和v1.2。兩個版本內(nèi)容上差不多,但就可讀性上來講,我還是推薦你讀 v1.2的。因為在v1.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)容,這里是以運行在x86上面的 UNIX System V.4 作為例子介紹

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

            這里多說些廢話:別忘了 Linus 在實現(xiàn)Linux的第一個版本的時候,就是看了介紹Unix內(nèi)部細節(jié)的書:《The of the Unix Operating System》,得到很多啟發(fā)。這本書對應(yīng)的操作系統(tǒng)是System V 的第二個Release。這本書介紹了操作系統(tǒng)的很多設(shè)計觀念,并且行文簡單易懂。所以雖然現(xiàn)在的Linux也吸取了其他很多Unix變種的設(shè)計理念,但是如果你想研究學(xué)習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)上一起運行,也會浪費掉寶貴的物理內(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) 在運行時,動態(tài)鏈接器(dynamic linker)拿它和一個Executable file以及另外一些 Shared object file 來一起處理,在Linux系統(tǒng)里面創(chuàng)建一個進程映像。

            以上所提到的 link editor 以及 dynamic linker 是什么東西,你可以參考我們基本概念中的相關(guān)文章。對于什么是編譯器,匯編器等你應(yīng)該也已經(jīng)知道,在這里只是使用他們而不再對他們進行詳細介紹。為了下面的敘述方便,你可以下載test.tar.gz包,解壓縮后使用"make"進行編譯。編譯完成后,會在目錄中生成一系列的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的組成格式?這是因為ELF格式需要使用在兩種場合:

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

            b) 組成可執(zhí)行文件或者可被共享的對象文件,以在運行時內(nèi)存中進程映像的構(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ī)范來。在實際寫代碼支持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ù)類型

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

            a) 這個sum.o的進入點是0x0(e_entry),這表面Relocatable objects不會有程序進入點。所謂程序進入點是指當程序真正執(zhí)行起來的時候,其第一條要運行的指令的運行時地址。因為Relocatable objects file只是供再鏈接而已,所以它不存在進入點。而可執(zhí)行文件test和動態(tài)庫.so都存在所謂的進入點,你可以用 readelf -h 看看。后面我們的文章中會介紹可執(zhí)行文件的e_entry指向C庫中的_start,而動態(tài)庫.so中的進入點指向 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。因為sum.o僅僅是參與link editor鏈接的可重定位文件,而不參與最后進程映像的構(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)標志。比方.text section 里面存儲的是代碼,所以就是只讀的(X);.data和.bss里面存放的都是可寫的(W)數(shù)據(jù)(非在堆棧中定義的數(shù)據(jù)),只不過前者存的是初始化過的數(shù)據(jù),比方程序中定義的賦過初值的全局變量等;而后者里面存儲的是未經(jīng)過初始化的數(shù)據(jù)。因為未經(jīng)過初始化就意味著不確定這些數(shù)據(jù)剛開始的時候會有些什么樣的值,所以針對對象文件來說,它就沒必要為了存儲這些數(shù)據(jù)而在文件內(nèi)多留出一塊空間,因此.bss section的大小總是為0。后面會看到,當可執(zhí)行程序被執(zhí)行的時候,動態(tài)連接器會在內(nèi)存中開辟一定大小的空間來存放這些未初始化的數(shù)據(jù),里面的內(nèi)存單元都被初始化成0。可執(zhí)行程序文件中雖然沒有長度非0的 .bss section,但卻記錄有在程序運行時,需要開辟多大的空間來容納這些未初始化的數(shù)據(jù)。

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

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

            我們?nèi)匀豢梢允褂?readelf -x SecNum 來傾印出不同 section 中的內(nèi)容。但是,無奈其輸出結(jié)果都是機器碼,對我們?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 的選項 -d 表示要對由 -j 選擇項指定的 section 內(nèi)容進行反匯編,也就是由機器碼出發(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,也就是十進制值 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é)果,因為后續(xù)文章中將會使用大量的這種命令。上面結(jié)果中的十六進制數(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ù)名稱、所定義過的變量名稱等等。。。當對象文件中其他地方需要和一個這樣的字符串相關(guān)聯(lián)的時候,往往會在對應(yīng)的地方存儲 .strtab section 中的索引值。比方下面將要介紹的符號表 .symtab section 中,有一個條目是用來描述符號 gv_inited 的,那么在該條目中就會有一個字段(st_name)記錄著字符串 gv_inited 在 .strtab section 中的索引 7 。 .shstrtab 也是字符串表,只不過其中存儲的是 section 的名字,而非所函數(shù)或者變量的名稱。

            字符串表在真正鏈接和生成進程映像過程中是不需要使用的,但是其對我們調(diào)試程序來說就特別有幫助,因為我們?nèi)丝雌饋碜钍娣倪€是自然形式的字符串,而非像天書一樣的數(shù)字符號。前面使用objdump來反匯編 .text section 的時候,之所以能看到定義了函數(shù) sum_func ,那也是因為存在這個字符串表的原因。當然起關(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 符號表條目

            上面結(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)庫中,符號的值表示的是運行時的內(nèi)存地址。

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

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

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

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

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

            如前面我們說過的,對象文件中的重定位條目,會構(gòu)成一個個單獨的 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)庫來說,就是該地址單元的運行時地址。上面 a條中的符號表內(nèi)索引和c條中的類型,一起構(gòu)成了結(jié)構(gòu)中的字段 r_info。

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

            重定位類型意味著如何去修改受影響的地址單元,也就是按照何種方式去計算需要最后放在受影響單元里面的值。具體的重定位類型有哪些,取決與特定的處理器架構(gòu),你可以參考相關(guān)規(guī)范。這種計算方式可以非常的簡單,比如在x386上的 R_386_32 類型,它規(guī)定只是將附加數(shù)加上符號的值作為所需要的值;該計算方式也可以是非常的復(fù)雜,比如老版本ARM平臺上的 R_ARM_PC26。在這篇文章的末尾,我會詳細介紹一種重定位類型: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)于鏈接器腳本,我們這里不予討論。

            一個單獨的 segment 通常會包含幾個不同的 sections,比方一個可被加載的、只讀的segment 通常就會包括可執(zhí)行代碼section .text、只讀的數(shù)據(jù)section .rodata以及給動態(tài)鏈接器使用的符號section .dymsym等等。section 是被鏈接器使用的,但是 segments 是被加載器所使用的。加載器會將所需要的 segment 加載到內(nèi)存空間中運行。和用 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 當中去。比方在索引為2的那個segment 中,總共有15個 sections 映射進來,其中包括我們前面提到過的 .text section。注意這個segment 有兩個標志: R 和 E。這個表示該segment是可讀的,也可執(zhí)行的。如果你看到標志中有W,那表示該segment是可寫的。

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

            ELF 程序頭表項

            上面類型為PHDR的segment,用來包含程序頭表本身。類型為INTERP的segment只包含一個 section,那就是 .interp。在這個section中,包含了動態(tài)鏈接過程中所使用的解釋器路徑和名稱。在Linux里面,這個解釋器實際上就是 /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īng)用程序通常都需要使用動態(tài)鏈接庫.so,就像 test 程序中所使用的 libsub.so 一樣。我們還是先大致說說程序在linux里面是怎么樣運行起來的吧。當你在 shell 中敲入一個命令要執(zhí)行時,內(nèi)核會幫我們創(chuàng)建一個新的進程,它在往這個新進程的進程空間里面加載進可執(zhí)行程序的代碼段和數(shù)據(jù)段后,也會加載進動態(tài)連接器(在Linux里面通常就是 /lib/ld-linux.so 符號鏈接所指向的那個程序,它本省就是一個動態(tài)庫)的代碼段和數(shù)據(jù)。在這之后,內(nèi)核將控制傳遞給動態(tài)鏈接庫里面的代碼。動態(tài)連接器接下來負責加載該命令應(yīng)用程序所需要使用的各種動態(tài)庫。加載完畢,動態(tài)連接器才將控制傳遞給應(yīng)用程序的main函數(shù)。如此,你的應(yīng)用程序才得以運行。

            這里說的只是大致的應(yīng)用程序啟動運行過程,更詳細的,我們會在后續(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)庫后,它還會做更進一步的鏈接,這就是所謂的動態(tài)鏈接。為了讓動態(tài)連接器能成功的完成動態(tài)鏈接過程,在前面運行的link editor需要在應(yīng)用程序可執(zhí)行文件中生成數(shù)個特殊的 sections,比方 .dynamic、.dynsym、.got和.plt等等。這些內(nèi)容我們會在后面的文章中進行討論。

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

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

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

            [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ù)據(jù)segment中開頭一部分也映射進了 text segment 中去;同樣的,進程虛擬內(nèi)存空間中的 data segment 從 08049000 開始,而可執(zhí)行文件中指定的是從 0x0804973c 開始。所以加載器也把代碼segment中末尾一部分也映射進了 data segment 中去了。

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

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

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

            或許,咱們舉一個實際例子來闡述可能對你更有用。在我們的 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,這是因為偏移 0x54 字節(jié)的地方原本應(yīng)該放著 sum_func 函數(shù)的地址,但現(xiàn)在因為 sum_func 定義在 sum.o 中,所以這個地方就是重定位需要做 patch 的地址單元所在處。我們注意到,這個地址單元的值為 0xfffffffc,也就是十進制的 -4(計算機中數(shù)是用補碼表示的)。所以,參與重定位運算的變量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呢?從正向去計算這兩個變量的值比較麻煩。盡管我們知道,在Linux里面,鏈接可執(zhí)行程序時所使用的默認的鏈接器腳本將最后可執(zhí)行程序的 .text segment 起始地址設(shè)置在 0x08048000的位置。但是,從這個地址出發(fā),去尋找符號(函數(shù))sub_func 和 上面受重定位影響的地址單元的運行時地址的話,需要經(jīng)過很多人工計算,所以比較麻煩。

            相反的,我們使用objdump工具像下面這樣分析最終鏈接生成的可執(zhí)行程序 ./test 的 .text segment 段,看看函數(shù) sum_func 和 那個受影響單元的運行時地址到底是多少,這是反向的查看鏈接器的鏈接結(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 分配的運行時地址是 0x0804860c,所以變量S的值就是 0x0804860c。那么變量P呢?它表示的是重定位所影響地址單元的運行地址。如果要計算這個地址,我們可以先看看 main 函數(shù)的運行時地址,再加上0x54字節(jié)的偏移來得到。從上面看出 main 函數(shù)的運行時地址為 0x08048540,所以重定位所影響地址單元的運行時地址為 0x08048540+0x54 = 0x08048594。所以重定位計算的最終結(jié)果為:

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

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

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

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

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


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


            久久亚洲综合色一区二区三区| 亚洲伊人久久成综合人影院 | 久久人人爽人人精品视频| 久久国产精品99精品国产| 久久笫一福利免费导航 | 亚洲乱亚洲乱淫久久| AAA级久久久精品无码片| 欧美亚洲色综久久精品国产| 日韩欧美亚洲综合久久 | 久久久久无码精品国产app| 国产成人久久精品麻豆一区| 久久精品国产福利国产秒| 狠狠干狠狠久久| 99久久精品国产综合一区| 久久996热精品xxxx| 亚洲精品无码专区久久同性男| 久久亚洲国产精品五月天婷| 一本大道久久东京热无码AV | 亚洲国产精品无码久久青草| 四虎国产精品成人免费久久| 亚洲中文久久精品无码ww16| 久久精品国产亚洲av麻豆色欲| 99精品久久精品| 久久久久久国产精品美女 | 伊人久久精品影院| 少妇人妻88久久中文字幕| 久久这里只有精品久久| 精品久久久久中文字| 久久久www免费人成精品| 97精品国产91久久久久久| 久久精品女人天堂AV麻| 亚洲精品白浆高清久久久久久| 久久精品国产第一区二区三区| 97久久精品人人澡人人爽| 亚洲精品国产自在久久| 国内精品久久久久影院日本 | 国产精品久久久久乳精品爆| 国产成人无码精品久久久性色| 亚洲一本综合久久| 亚洲精品无码久久千人斩| 精品久久久久久久久久中文字幕|