程序的鏈接和裝入存在著多種方法,而如今最為流行的當(dāng)屬動(dòng)態(tài)鏈接、動(dòng)態(tài)裝入方法。本文首先回顧了鏈接器和裝入器的基本工作原理及這一技術(shù)的發(fā)展歷史,然后通過(guò)實(shí)際的例子剖析了Linux系統(tǒng)下動(dòng)態(tài)鏈接的實(shí)現(xiàn)。了解底層關(guān)鍵技術(shù)的實(shí)現(xiàn)細(xì)節(jié)對(duì)系統(tǒng)分析和設(shè)計(jì)人員無(wú)疑是必須的,尤其當(dāng)我們?cè)诿鎸?duì)實(shí)時(shí)系統(tǒng),需要對(duì)程序執(zhí)行時(shí)的時(shí)空效率有著精確的度量和把握時(shí),這種知識(shí)更顯重要。
鏈接器和裝入器的基本工作原理
一個(gè)程序要想在內(nèi)存中運(yùn)行,除了編譯之外還要經(jīng)過(guò)鏈接和裝入這兩個(gè)步驟。從程序員的角度來(lái)看,引入這兩個(gè)步驟帶來(lái)的好處就是可以直接在程序中使用printf和errno這種有意義的函數(shù)名和變量名,而不用明確指明printf和errno在標(biāo)準(zhǔn)C庫(kù)中的地址。當(dāng)然,為了將程序員從早期直接使用地址編程的夢(mèng)魘中解救出來(lái),編譯器和匯編器在這當(dāng)中做出了革命性的貢獻(xiàn)。編譯器和匯編器的出現(xiàn)使得程序員可以在程序中使用更具意義的符號(hào)來(lái)為函數(shù)和變量命名,這樣使得程序在正確性和可讀性等方面都得到了極大的提高。但是隨著C語(yǔ)言這種支持分別編譯的程序設(shè)計(jì)語(yǔ)言的流行,一個(gè)完整的程序往往被分割為若干個(gè)獨(dú)立的部分并行開(kāi)發(fā),而各個(gè)模塊間通過(guò)函數(shù)接口或全局變量進(jìn)行通訊。這就帶來(lái)了一個(gè)問(wèn)題,編譯器只能在一個(gè)模塊內(nèi)部完成符號(hào)名到地址的轉(zhuǎn)換工作,不同模塊間的符號(hào)解析由誰(shuí)來(lái)做呢?比如前面所舉的例子,調(diào)用printf的用戶(hù)程序和實(shí)現(xiàn)了printf的標(biāo)準(zhǔn)C庫(kù)顯然就是兩個(gè)不同的模塊。實(shí)際上,這個(gè)工作是由鏈接器來(lái)完成的。
為了解決不同模塊間的鏈接問(wèn)題,鏈接器主要有兩個(gè)工作要做――符號(hào)解析和重定位:
符號(hào)解析:當(dāng)一個(gè)模塊使用了在該模塊中沒(méi)有定義過(guò)的函數(shù)或全局變量時(shí),編譯器生成的符號(hào)表會(huì)標(biāo)記出所有這樣的函數(shù)或全局變量,而鏈接器的責(zé)任就是要到別的模塊中去查找它們的定義,如果沒(méi)有找到合適的定義或者找到的合適的定義不唯一,符號(hào)解析都無(wú)法正常完成。
重定位:編譯器在編譯生成目標(biāo)文件時(shí),通常都使用從零開(kāi)始的相對(duì)地址。然而,在鏈接過(guò)程中,鏈接器將從一個(gè)指定的地址開(kāi)始,根據(jù)輸入的目標(biāo)文件的順序以段為單位將它們一個(gè)接一個(gè)的拼裝起來(lái)。除了目標(biāo)文件的拼裝之外,在重定位的過(guò)程中還完成了兩個(gè)任務(wù):一是生成最終的符號(hào)表;二是對(duì)代碼段中的某些位置進(jìn)行修改,所有需要修改的位置都由編譯器生成的重定位表指出。
舉個(gè)簡(jiǎn)單的例子,上面的概念對(duì)讀者來(lái)說(shuō)就一目了然了。假如我們有一個(gè)程序由兩部分構(gòu)成,m.c中的main函數(shù)調(diào)用f.c中實(shí)現(xiàn)的函數(shù)sum:
/* m.c */int i = 1;int j = 2;extern int sum();void main(){ int s; s = sum(i, j);/* f.c */int sum(int i, int j){ return i + j;}
在Linux用gcc分別將兩段源程序編譯成目標(biāo)文件:
$ gcc -c m.c$ gcc -c f.c
我們通過(guò)objdump來(lái)看看在編譯過(guò)程中生成的符號(hào)表和重定位表:
$ objdump -x m.o……SYMBOL TABLE:……00000000 gO .data 00000004 i00000004 gO .data 00000004 j00000000 gF .text 00000021 main00000000 *UND* 00000000 sumRELOCATION RECORDS FOR [.text]:OFFSET TYPE VALUE00000007 R_386_32 j0000000d R_386_32 i00000013 R_386_PC32 sum
首先,我們注意到符號(hào)表里面的sum被標(biāo)記為UND(undefined),也就是在m.o中沒(méi)有定義,所以將來(lái)要通過(guò)ld(Linux下的鏈接器)的符號(hào)解析功能到別的模塊中去查找是否存在函數(shù)sum的定義。另外,在重定位表中有三條記錄,指出了在重定位過(guò)程中代碼段中三處需要修改的位置,分別位于7、d和13。下面以一種更加直觀的方式來(lái)看一下這三個(gè)位置:
$ objdump -dx m.oDisassembly of section .text:00000000 以sum為例,對(duì)函數(shù)sum的調(diào)用是通過(guò)call指令實(shí)現(xiàn)的,使用IP相對(duì)尋址方式。可以看到,在目標(biāo)文件m.o中,call指令位于從零開(kāi)始的相對(duì)地址12的位置,這里存放的e8是call的操作碼,而從13開(kāi)始的4個(gè)字節(jié)存放著sum相對(duì)call的下一條指令add的偏移。顯然,在鏈接之前這個(gè)偏移量是不知道的,所以將來(lái)要來(lái)修改13這里的代碼。那現(xiàn)在這里為什么存放著0xfffffffc(注意Intel的CPU使用little endian的編址方式)呢?這大概是出于安全的考慮,因?yàn)?xfffffffc正是-4的補(bǔ)碼表示(讀者可以在gdb中使用p /x -4查看),而call指令本身占用了5個(gè)字節(jié),因此無(wú)論如何call指令中的偏移量不可能是-4。我們?cè)倏纯粗囟ㄎ恢骳all指令中的這個(gè)偏移量被修改成了什么: $ gcc m.o f.o$ objdump -dj .text a.out | lessDisassembly of section .text:……080482c4 可以看到經(jīng)過(guò)重定位之后,call指令中的偏移量修改成0x0000000d了,簡(jiǎn)單的計(jì)算告訴我們:0x080482e8-0x80482db=0xd。這樣,經(jīng)過(guò)重定位之后最終的可執(zhí)行程序就生成了。 可執(zhí)行程序生成后,下一步就是將其裝入內(nèi)存運(yùn)行。Linux下的編譯器(C語(yǔ)言)是cc1,匯編器是as,鏈接器是ld,但是并沒(méi)有一個(gè)實(shí)際的程序?qū)?yīng)裝入器這個(gè)概念。實(shí)際上,將可執(zhí)行程序裝入內(nèi)存運(yùn)行的功能是由execve(2)這一系統(tǒng)調(diào)用實(shí)現(xiàn)的。簡(jiǎn)單來(lái)講,程序的裝入主要包含以下幾個(gè)步驟: ? 鏈接和裝入技術(shù)的發(fā)展史 一個(gè)程序要想裝入內(nèi)存運(yùn)行必然要先經(jīng)過(guò)編譯、鏈接和裝入這三個(gè)階段,雖然是這樣一個(gè)大家聽(tīng)起來(lái)耳熟能詳?shù)母拍睿诓僮飨到y(tǒng)發(fā)展的過(guò)程中卻已經(jīng)經(jīng)歷了多次重大變革。簡(jiǎn)單來(lái)講,可以將其劃分為以下三個(gè)階段: 1. 靜態(tài)鏈接、靜態(tài)裝入 這種方法最早被采用,其特點(diǎn)是簡(jiǎn)單,不需要操作系統(tǒng)提供任何額外的支持。像C這樣的編程語(yǔ)言從很早開(kāi)始就已經(jīng)支持分別編譯了,程序的不同模塊可以并行開(kāi)發(fā),然后獨(dú)立編譯為相應(yīng)的目標(biāo)文件。在得到了所有的目標(biāo)文件后,靜態(tài)鏈接、靜態(tài)裝入的做法是將所有目標(biāo)文件鏈接成一個(gè)可執(zhí)行映象,隨后在創(chuàng)建進(jìn)程時(shí)將該可執(zhí)行映象一次全部裝入內(nèi)存。舉個(gè)簡(jiǎn)單的例子,假設(shè)我們開(kāi)發(fā)了兩個(gè)程序Prog1和Prog2,Prog1由main1.c、utilities.c以及errhdl1.c三部分組成,分別對(duì)應(yīng)程序的主框架、一些公用的輔助函數(shù)(其作用相當(dāng)于庫(kù))以及錯(cuò)誤處理部分,這三部分代碼編譯后分別得到各自對(duì)應(yīng)的目標(biāo)文件main1.o、utilities.o以及errhdl1.o。同樣,Prog2由main2.c、utilities.c以及errhdl2.c三部分組成,三部分代碼編譯后分別得到各自對(duì)應(yīng)的目標(biāo)文件main2.o、utilities.o以及errhdl2.o。值得注意的是,這里Prog1和Prog2使用了相同的公用輔助函數(shù)utilities.o。當(dāng)我們采用靜態(tài)鏈接、靜態(tài)裝入的方法,同時(shí)運(yùn)行這兩個(gè)程序時(shí)內(nèi)存和硬盤(pán)的使用情況如圖1所示: ? 可以看到,首先就硬盤(pán)的使用來(lái)講,雖然兩個(gè)程序共享使用了utilities,但這并沒(méi)有在硬盤(pán)保存的可執(zhí)行程序映象上體現(xiàn)出來(lái)。相反,utilities.o被鏈接進(jìn)了每一個(gè)用到它的程序的可執(zhí)行映象。內(nèi)存的使用也是如此,操作系統(tǒng)在創(chuàng)建進(jìn)程時(shí)將程序的可執(zhí)行映象一次全部裝入內(nèi)存,之后進(jìn)程才能開(kāi)始運(yùn)行。如前所述,采用這種方法使得操作系統(tǒng)的實(shí)現(xiàn)變得非常簡(jiǎn)單,但其缺點(diǎn)也是顯而易見(jiàn)的。首先,既然兩個(gè)程序使用的是相同的utilities.o,那么我們只要在硬盤(pán)上保存utilities.o的一份拷貝應(yīng)該就足夠了;另外,假如程序在運(yùn)行過(guò)程中沒(méi)有出現(xiàn)任何錯(cuò)誤,那么錯(cuò)誤處理部分的代碼就不應(yīng)該被裝入內(nèi)存。因此靜態(tài)鏈接、靜態(tài)裝入的方法不但浪費(fèi)了硬盤(pán)空間,同時(shí)也浪費(fèi)了內(nèi)存空間。由于早期系統(tǒng)的內(nèi)存資源十分寶貴,所以后者對(duì)早期的系統(tǒng)來(lái)講更加致命。 2. 靜態(tài)鏈接、動(dòng)態(tài)裝入 既然采用靜態(tài)鏈接、靜態(tài)裝入的方法弊大于利,我們來(lái)看看人們是如何解決這一問(wèn)題的。由于內(nèi)存緊張的問(wèn)題在早期的系統(tǒng)中顯得更加突出,因此人們首先想到的是要解決內(nèi)存使用效率不高這一問(wèn)題,于是便提出了動(dòng)態(tài)裝入的思想。其想法是非常簡(jiǎn)單的,即一個(gè)函數(shù)只有當(dāng)它被調(diào)用時(shí),其所在的模塊才會(huì)被裝入內(nèi)存。所有的模塊都以一種可重定位的裝入格式存放在磁盤(pán)上。首先,主程序被裝入內(nèi)存并開(kāi)始運(yùn)行。當(dāng)一個(gè)模塊需要調(diào)用另一個(gè)模塊中的函數(shù)時(shí),首先要檢查含有被調(diào)用函數(shù)的模塊是否已裝入內(nèi)存。如果該模塊尚未被裝入內(nèi)存,那么將由負(fù)責(zé)重定位的鏈接裝入器將該模塊裝入內(nèi)存,同時(shí)更新此程序的地址表以反應(yīng)這一變化。之后,控制便轉(zhuǎn)移到了新裝入的模塊中被調(diào)用的函數(shù)那里。 動(dòng)態(tài)裝入的優(yōu)點(diǎn)在于永遠(yuǎn)不會(huì)裝入一個(gè)使用不到的模塊。如果程序中存在著大量像出錯(cuò)處理函數(shù)這種用于處理小概率事件的代碼,使用這種方法無(wú)疑是卓有成效的。在這種情況下,即使整個(gè)程序可能很大,但是實(shí)際用到(因此被裝入到內(nèi)存中)的部分實(shí)際上可能非常小。 仍然以上面提到的兩個(gè)程序Prog1和Prog2為例,假如Prog1運(yùn)行過(guò)程中出現(xiàn)了錯(cuò)誤而Prog2在運(yùn)行過(guò)程中沒(méi)有出現(xiàn)任何錯(cuò)誤。當(dāng)我們采用靜態(tài)鏈接、動(dòng)態(tài)裝入的方法,同時(shí)運(yùn)行這兩個(gè)程序時(shí)內(nèi)存和硬盤(pán)的使用情況如圖2所示: ? 圖 2采用靜態(tài)鏈接、動(dòng)態(tài)裝入方法,同時(shí)運(yùn)行Prog1和Prog2時(shí)內(nèi)存和硬盤(pán)的使用情況 可以看到,當(dāng)程序中存在著大量像錯(cuò)誤處理這樣使用概率很小的模塊時(shí),采用靜態(tài)鏈接、動(dòng)態(tài)裝入的方法在內(nèi)存的使用效率上就體現(xiàn)出了相當(dāng)大的優(yōu)勢(shì)。到此為止,人們已經(jīng)向理想的目標(biāo)邁進(jìn)了一部,但是問(wèn)題還沒(méi)有完全解決――內(nèi)存的使用效率提高了,硬盤(pán)呢? 3. 動(dòng)態(tài)鏈接、動(dòng)態(tài)裝入 采用靜態(tài)鏈接、動(dòng)態(tài)裝入的方法后看似只剩下硬盤(pán)空間使用效率不高的問(wèn)題了,實(shí)際上內(nèi)存使用效率不高的問(wèn)題仍然沒(méi)有完全解決。圖2中,既然兩個(gè)程序用到的是相同的utilities.o,那么理想的情況是系統(tǒng)中只保存一份utilities.o的拷貝,無(wú)論是在內(nèi)存中還是在硬盤(pán)上,于是人們想到了動(dòng)態(tài)鏈接。 在使用動(dòng)態(tài)鏈接時(shí),需要在程序映象中每個(gè)調(diào)用庫(kù)函數(shù)的地方打一個(gè)樁(stub)。stub是一小段代碼,用于定位已裝入內(nèi)存的相應(yīng)的庫(kù);如果所需的庫(kù)還不在內(nèi)存中,stub將指出如何將該函數(shù)所在的庫(kù)裝入內(nèi)存。 當(dāng)執(zhí)行到這樣一個(gè)stub時(shí),首先檢查所需的函數(shù)是否已位于內(nèi)存中。如果所需函數(shù)尚不在內(nèi)存中,則首先需要將其裝入。不論怎樣,stub最終將被調(diào)用函數(shù)的地址替換掉。這樣,在下次運(yùn)行同一個(gè)代碼段時(shí),同樣的庫(kù)函數(shù)就能直接得以運(yùn)行,從而省掉了動(dòng)態(tài)鏈接的額外開(kāi)銷(xiāo)。由此,用到同一個(gè)庫(kù)的所有進(jìn)程在運(yùn)行時(shí)使用的都是這個(gè)庫(kù)的同一份拷貝。 下面我們就來(lái)看看上面提到的兩個(gè)程序Prog1和Prog2在采用動(dòng)態(tài)鏈接、動(dòng)態(tài)裝入的方法,同時(shí)運(yùn)行這兩個(gè)程序時(shí)內(nèi)存和硬盤(pán)的使用情況(見(jiàn)圖3)。仍然假設(shè)Prog1運(yùn)行過(guò)程中出現(xiàn)了錯(cuò)誤而Prog2在運(yùn)行過(guò)程中沒(méi)有出現(xiàn)任何錯(cuò)誤。 ? 圖 3采用動(dòng)態(tài)鏈接、動(dòng)態(tài)裝入方法,同時(shí)運(yùn)行Prog1和Prog2時(shí)內(nèi)存和硬盤(pán)的使用情況 圖中,無(wú)論是硬盤(pán)還是內(nèi)存中都只存在一份utilities.o的拷貝。內(nèi)存中,兩個(gè)進(jìn)程通過(guò)將地址映射到相同的utilities.o實(shí)現(xiàn)對(duì)其的共享。動(dòng)態(tài)鏈接的這一特性對(duì)于庫(kù)的升級(jí)(比如錯(cuò)誤的修正)是至關(guān)重要的。當(dāng)一個(gè)庫(kù)升級(jí)到一個(gè)新版本時(shí),所有用到這個(gè)庫(kù)的程序?qū)⒆詣?dòng)使用新的版本。如果不使用動(dòng)態(tài)鏈接技術(shù),那么所有這些程序都需要被重新鏈接才能得以訪(fǎng)問(wèn)新版的庫(kù)。為了避免程序意外使用到一些不兼容的新版的庫(kù),通常在程序和庫(kù)中都包含各自的版本信息。內(nèi)存中可能會(huì)同時(shí)存在著一個(gè)庫(kù)的幾個(gè)版本,但是每個(gè)程序可以通過(guò)版本信息來(lái)決定它到底應(yīng)該使用哪一個(gè)。如果對(duì)庫(kù)只做了微小的改動(dòng),庫(kù)的版本號(hào)將保持不變;如果改動(dòng)較大,則相應(yīng)遞增版本號(hào)。因此,如果新版庫(kù)中含有與早期不兼容的改動(dòng),只有那些使用新版庫(kù)進(jìn)行編譯的程序才會(huì)受到影響,而在新版庫(kù)安裝之前進(jìn)行過(guò)鏈接的程序?qū)⒗^續(xù)使用以前的庫(kù)。這樣的系統(tǒng)被稱(chēng)作共享庫(kù)系統(tǒng)。 ? Linux下動(dòng)態(tài)鏈接的實(shí)現(xiàn) 如今我們?cè)贚inux下編程用到的庫(kù)(像libc、QT等等)大多都同時(shí)提供了動(dòng)態(tài)鏈接庫(kù)和靜態(tài)鏈接庫(kù)兩個(gè)版本的庫(kù),而gcc在編譯鏈接時(shí)如果不加-static選項(xiàng)則默認(rèn)使用系統(tǒng)中的動(dòng)態(tài)鏈接庫(kù)。對(duì)于動(dòng)態(tài)鏈接庫(kù)的原理大多數(shù)的書(shū)本上只是進(jìn)行了泛泛的介紹,在此筆者將通過(guò)在實(shí)際系統(tǒng)中反匯編出的代碼向讀者展示這一技術(shù)在Linux下的實(shí)現(xiàn)。 下面是個(gè)最簡(jiǎn)單的C程序hello.c: ? #include ? 在Linux下我們可以使用gcc將其編譯成可執(zhí)行文件a.out: ? $ gcc hello.c ? 程序里用到了printf,它位于標(biāo)準(zhǔn)C庫(kù)中,如果在用gcc編譯時(shí)不加-static的話(huà),默認(rèn)是使用libc.so,也就是動(dòng)態(tài)鏈接的標(biāo)準(zhǔn)C庫(kù)。在gdb中可以看到編譯后printf對(duì)應(yīng)如下代碼 : ? $ gdb -q a.out(gdb) disassemble printfDump of assembler code for function printf:0x8048310 ? 這也就是通常在書(shū)本上以及前面提到的打樁(stub)過(guò)程,顯然這并不是真正的printf函數(shù)。這段stub代碼的作用在于到libc.so中去查找真正的printf。 ? (gdb) x /w 0x80495a40x80495a4 <_GLOBAL_OFFSET_TABLE_+24>: 0x08048316 ? 可以看到0x80495a4處存放的0x08048316正是pushl $0x18這條指令的地址,所以第一條jmp指令沒(méi)有起到任何作用,其作用就像空操作指令nop一樣。當(dāng)然這是在我們第一次調(diào)用printf時(shí),其真正的作用是在今后再次調(diào)用printf時(shí)體現(xiàn)出來(lái)的。第二條jmp指令的目的地址是plt,也就是procedure linkage table,其內(nèi)容可以通過(guò)objdump命令查看,我們感興趣的就是下面這兩條對(duì)程序的控制流有影響的指令: ? $ objdump -dx a.out……080482d0 >.plt>: 80482d0: ff 35 90 95 04 08 pushl 0x8049590 80482d6: ff 25 94 95 04 08 jmp *0x8049594…… ? 第一條push指令將got(global offset table)中與printf相關(guān)的表項(xiàng)地址壓入堆棧,之后jmp到內(nèi)存單元0x8049594中所存放的地址0x4000a960處。這里需要注意的一點(diǎn)是,在查看got之前必須先將程序a.out啟動(dòng)運(yùn)行,否則通過(guò)gdb中的x命令在0x8049594處看到的結(jié)果是不正確的。 ? (gdb) b mainBreakpoint 1 at 0x8048406(gdb) rStarting program: a.outBreakpoint 1, 0x08048406 in main ()(gdb) x /w 0x80495940x8049594 <_GLOBAL_OFFSET_TABLE_+8>: 0x4000a960(gdb) disassemble 0x4000a960Dump of assembler code for function _dl_runtime_resolve:0x4000a960 <_dl_runtime_resolve>: pushl %eax0x4000a961 <_dl_runtime_resolve+1>: pushl %ecx0x4000a962 <_dl_runtime_resolve+2>: pushl %edx0x4000a963 <_dl_runtime_resolve+3>: movl 0x10(%esp,1),%edx0x4000a967 <_dl_runtime_resolve+7>: movl 0xc(%esp,1),%eax0x4000a96b <_dl_runtime_resolve+11>: call 0x4000a740 ? 前面三條push指令執(zhí)行之后堆棧里面的內(nèi)容如下: ? 下面將0x18存入edx,0x8049590存入eax,有了這兩個(gè)參數(shù),fixup就可以找到printf在libc.so中的地址。當(dāng)fixup返回時(shí),該地址已經(jīng)保存在了eax中。xchg指令執(zhí)行完之后堆棧中的內(nèi)容如下: ? 最妙的要數(shù)接下來(lái)的ret指令的用法,這里ret實(shí)際上被當(dāng)成了call來(lái)使用。ret $0x8之后控制便轉(zhuǎn)移到了真正的printf函數(shù)那里,并且清掉了堆棧上的0x18和0x8049584這兩個(gè)已經(jīng)沒(méi)用的參數(shù),這時(shí)堆棧便成了下面的樣子: ? 而這正是我們所期望的結(jié)果。應(yīng)該說(shuō)這里ret的用法與Linux內(nèi)核啟動(dòng)后通過(guò)iret指令實(shí)現(xiàn)由內(nèi)核態(tài)切換到用戶(hù)態(tài)的做法有著異曲同工之妙。很多人都聽(tīng)說(shuō)過(guò)中斷指令int可以實(shí)現(xiàn)用戶(hù)態(tài)到內(nèi)核態(tài)這種優(yōu)先級(jí)由低到高的切換,在接受完系統(tǒng)服務(wù)后iret指令負(fù)責(zé)將優(yōu)先級(jí)重新降至用戶(hù)態(tài)的優(yōu)先級(jí)。然而系統(tǒng)啟動(dòng)時(shí)首先是處于內(nèi)核態(tài)高優(yōu)先級(jí)的,Intel i386并沒(méi)有單獨(dú)提供一條特殊的指令用于在系統(tǒng)啟動(dòng)完成后降低優(yōu)先級(jí)以運(yùn)行用戶(hù)程序。其實(shí)這個(gè)問(wèn)題很簡(jiǎn)單,只要反用iret就可以了,就像這里將ret當(dāng)作call使用一樣。另外,fixup函數(shù)執(zhí)行完還有一個(gè)副作用,就是在got中與printf相關(guān)的表項(xiàng)(也就是地址為0x80495a4的內(nèi)存單元)中填上查找到的printf函數(shù)在動(dòng)態(tài)鏈接庫(kù)中的地址。這樣當(dāng)我們?cè)俅握{(diào)用printf函數(shù)時(shí),其地址就可以直接從got中得到,從而省去了通過(guò)fixup查找的過(guò)程。也就是說(shuō)got在這里起到了cache的作用。
評(píng)論
查看更多