• <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, 評(píng)論 - 101, 引用 - 0
            數(shù)據(jù)加載中……

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

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

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

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

            值得一說(shuō)的是,雖然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)的另外一個(gè)分支BSD Unix,Linux系統(tǒng)更靠近 System V 系統(tǒng)。所以關(guān)于操作系統(tǒng)特定的ELF內(nèi)容,你可以直接參考v1.2標(biāo)準(zhǔn)中的內(nèi)容。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            ELF 文件頭

            等等,為什么會(huì)有左右兩個(gè)很類似的圖來(lái)說(shuō)明ELF的組成格式?這是因?yàn)镋LF格式需要使用在兩種場(chǎng)合:

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

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

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

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

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

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

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

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

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

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

            那什么是所謂 sections 呢?可以說(shuō),sections 是在ELF文件里頭,用以裝載內(nèi)容數(shù)據(jù)的最小容器。在ELF文件里面,每一個(gè) 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 里面裝載了符號(hào)信息;

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

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

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

            ELF section header entry

            我們可以像下面那樣來(lái)使用 readelf 工具來(lái)查看可重定位對(duì)象文件 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)

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

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

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

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

            我們?nèi)匀豢梢允褂?readelf -x SecNum 來(lái)傾印出不同 section 中的內(nèi)容。但是,無(wú)奈其輸出結(jié)果都是機(jī)器碼,對(duì)我們?nèi)藖?lái)說(shuō)不具備可讀性。所以我們換用 binutils 包中的另外一個(gè)工具 objdump 來(lái)看看這些 sections 中到底具有哪些內(nèi)容,先來(lá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 表示要對(duì)由 -j 選擇項(xiàng)指定的 section 內(nèi)容進(jìn)行反匯編,也就是由機(jī)器碼出發(fā),推導(dǎo)出相應(yīng)的匯編指令。上面結(jié)果顯示在 sum.o 對(duì)象文件的 .text 中只是包含了函數(shù) sum_func 的定義。用同樣的方法,我們來(lái)看看 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                                         ....

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

            我們接下來(lái)來(lái)看看字符串表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)容中可以找到。盡管這個(gè)命令的輸出結(jié)果不是那么具有可讀性,但我們還是得來(lái)說(shuō)一說(shuō)如何看這個(gè)結(jié)果,因?yàn)楹罄m(xù)文章中將會(huì)使用大量的這種命令。上面結(jié)果中的十六進(jìn)制數(shù)據(jù)部分從右到左看是地址遞增的方向,而字符內(nèi)容部分從左到右看是地址遞增的方向。所以,在 .strtab section 中,按照地址遞增的方向來(lái)看,各字節(jié)的內(nèi)容依次是 0x00、0x73、0x75、0x6d、0x2e ....,也就是字符 、's'、'u'、'm'、'.' ... 等。如果還是看不太明白,你可以使用 hexdump 直接dumping出 .strtab section 開(kāi)頭(其偏移在文件內(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 中存儲(chǔ)著的都是以字符 為分割符的字符串,這些字符串所表示的內(nèi)容,通常是程序中定義的函數(shù)名稱、所定義過(guò)的變量名稱等等。。。當(dāng)對(duì)象文件中其他地方需要和一個(gè)這樣的字符串相關(guān)聯(lián)的時(shí)候,往往會(huì)在對(duì)應(yīng)的地方存儲(chǔ) .strtab section 中的索引值。比方下面將要介紹的符號(hào)表 .symtab section 中,有一個(gè)條目是用來(lái)描述符號(hào) gv_inited 的,那么在該條目中就會(huì)有一個(gè)字段(st_name)記錄著字符串 gv_inited 在 .strtab section 中的索引 7 。 .shstrtab 也是字符串表,只不過(guò)其中存儲(chǔ)的是 section 的名字,而非所函數(shù)或者變量的名稱。

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

            雖然我們同樣可以使用 readelf -x 來(lái)查看符號(hào)表(.symtab)section的內(nèi)容,但是其結(jié)果可讀性太差,我們換用 readelf -s 或者 objdump -t 來(lái)查看(前者輸出結(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

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

            ELF 符號(hào)表?xiàng)l目

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

            ELF 符號(hào)類型

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            我們可以用 readelf -l 來(lái)查看可執(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個(gè) segments。同時(shí),該結(jié)果也很明白顯示出了哪些 section 映射到哪一個(gè) segment 當(dāng)中去。比方在索引為2的那個(gè)segment 中,總共有15個(gè) sections 映射進(jìn)來(lái),其中包括我們前面提到過(guò)的 .text section。注意這個(gè)segment 有兩個(gè)標(biāo)志: R 和 E。這個(gè)表示該segment是可讀的,也可執(zhí)行的。如果你看到標(biāo)志中有W,那表示該segment是可寫(xiě)的。

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

            ELF 程序頭表項(xiàng)

            上面類型為PHDR的segment,用來(lái)包含程序頭表本身。類型為INTERP的segment只包含一個(gè) section,那就是 .interp。在這個(gè)section中,包含了動(dòng)態(tài)鏈接過(guò)程中所使用的解釋器路徑和名稱。在Linux里面,這個(gè)解釋器實(shí)際上就是 /lib/ ,這可以通過(guò)下面的 hexdump 看出來(lái):[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

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

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

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

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

            我們有多種選擇。最簡(jiǎn)單的是,在 test main 程序中插入一個(gè)循環(huán),然后在循環(huán)中放入 sleep() 的調(diào)用,這樣當(dāng)程序運(yùn)行到這個(gè)循環(huán)的時(shí)候,就會(huì)進(jìn)入“運(yùn)行-睡眠-運(yùn)行-睡眠”循環(huán)中。這樣我們就有機(jī)會(huì)去看它的虛擬內(nèi)存空間信息。另外一個(gè)方法,是使用調(diào)試器,如GDB。我們?cè)O(shè)置一個(gè)斷點(diǎn),然后在調(diào)試過(guò)程中讓test進(jìn)程在這個(gè)斷點(diǎn)處暫停,這樣我們也有機(jī)會(huì)獲得地址空間的信息。我們這里就使用這種方法。當(dāng)然,為了能讓GDB調(diào)試我們的 test,我們得在編譯的時(shí)候加上"-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 是用`括起來(lái)的,它不是單引號(hào),而是鍵盤(pán)上 Esc 字符下面的那個(gè)字符。從這個(gè)結(jié)果上可以看出,所有的段,其起始地址和結(jié)束地址(前面兩列)都是0x1000對(duì)齊的。結(jié)果中也列出了對(duì)應(yīng)的段是從哪里引過(guò)來(lái)的,比方動(dòng)態(tài)鏈接器/lib/ld-2.3.4.so、C函數(shù)庫(kù)和test程序本身。注意看test程序引入的代碼段起始地址是 0x08048000,這和我們 ELF 文件中指定的相同,但是結(jié)束地址卻是0x08049000,和文件中指定的不一致(0x08048000+0x0073c=0x0804873c)。這里,其實(shí)加載器也把數(shù)據(jù)segment中開(kāi)頭一部分也映射進(jìn)了 text segment 中去;同樣的,進(jìn)程虛擬內(nèi)存空間中的 data segment 從 08049000 開(kāi)始,而可執(zhí)行文件中指定的是從 0x0804973c 開(kāi)始。所以加載器也把代碼segment中末尾一部分也映射進(jìn)了 data segment 中去了。

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

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

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

            或許,咱們舉一個(gè)實(shí)際例子來(lái)闡述可能對(duì)你更有用。在我們的 test 程序中,test.c 的 main 函數(shù)中需要調(diào)用定義在 sum.o 中的 sum_func 函數(shù),所以link editor 在將 test.o/sum.o 聯(lián)結(jié)成可執(zhí)行文件 test 的時(shí)候,必須處理一個(gè)重定位,這個(gè)重定位就是 R_386_PC32 類型的。我們先用 objdump 來(lái)查看 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é)果所示,在離開(kāi) .text section 開(kāi)始 0x53 字節(jié)的地方,有一條call指令。這條指令是對(duì) 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 中,所以這個(gè)地方就是重定位需要做 patch 的地址單元所在處。我們注意到,這個(gè)地址單元的值為 0xfffffffc,也就是十進(jìn)制的 -4(計(jì)算機(jī)中數(shù)是用補(bǔ)碼表示的)。所以,參與重定位運(yùn)算的變量A就確定了,即是 -4。

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

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

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

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

            [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)行時(shí)地址是 0x0804860c,所以變量S的值就是 0x0804860c。那么變量P呢?它表示的是重定位所影響地址單元的運(yùn)行地址。如果要計(jì)算這個(gè)地址,我們可以先看看 main 函數(shù)的運(yùn)行時(shí)地址,再加上0x54字節(jié)的偏移來(lái)得到。從上面看出 main 函數(shù)的運(yùn)行時(shí)地址為 0x08048540,所以重定位所影響地址單元的運(yùn)行時(shí)地址為 0x08048540+0x54 = 0x08048594。所以重定位計(jì)算的最終結(jié)果為:

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

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

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

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

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

            国产午夜精品理论片久久| 国产精品亚洲综合专区片高清久久久 | 国内精品久久久久影院日本 | 综合久久久久久中文字幕亚洲国产国产综合一区首 | 久久精品国产精品国产精品污| 国产999精品久久久久久| 久久精品极品盛宴观看| 国产美女久久精品香蕉69| 国产综合精品久久亚洲| 久久久久AV综合网成人| 一级a性色生活片久久无少妇一级婬片免费放| 怡红院日本一道日本久久 | 国产精品久久久久乳精品爆| 一本色道久久综合狠狠躁| 国产精品美女久久久久av爽 | 国产99久久九九精品无码| 无码伊人66久久大杳蕉网站谷歌| 国内精品久久久久久久影视麻豆| 久久久噜噜噜久久中文福利| 伊人久久大香线蕉精品不卡| 国内精品久久久久久久久| 韩国免费A级毛片久久| 亚洲国产精品无码成人片久久 | 人妻无码αv中文字幕久久| 伊人久久大香线蕉综合5g | 99久久成人国产精品免费| 伊人久久大香线蕉av一区| 香港aa三级久久三级老师2021国产三级精品三级在 | 久久久久青草线蕉综合超碰| 久久精品国产黑森林| 久久精品国产第一区二区| 久久本道久久综合伊人| 97久久精品人人做人人爽| 久久精品国产亚洲麻豆| 香蕉久久夜色精品国产小说| 久久香蕉一级毛片| 99久久精品这里只有精品| 国产午夜精品久久久久九九电影| 精品久久久久久国产牛牛app| 久久99精品久久久久久不卡| 亚洲av日韩精品久久久久久a |