關(guān)于動(dòng)態(tài)鏈接原理性文章有很多,在此本人盡量以深入淺出和少量的篇幅將問(wèn)題闡述清楚,拋開(kāi)無(wú)關(guān)的擴(kuò)展。
一、linux加載可執(zhí)行文件時(shí)的存儲(chǔ)器映像
下面圖片所描述的??臻g,是linux加載應(yīng)用程序時(shí)所生成的:
首先,如果不更改內(nèi)核,那么linux系統(tǒng)加載程序(包括內(nèi)核里的子進(jìn)程)都是從0x08048000地址開(kāi)始的。當(dāng)加載器運(yùn)行時(shí),先對(duì)應(yīng)用程序中的可執(zhí)行文件進(jìn)行解析,將代碼段和數(shù)據(jù)段按照4kB對(duì)齊的方式,從0x08048000開(kāi)始往高地址放。緊接著就是存放堆,堆的增長(zhǎng)方向會(huì)按照箭頭所指往高地址增長(zhǎng)。
但是堆的空間是有限的,它只能增長(zhǎng)到0x3fffffff,從0x40000000開(kāi)始,需要存放動(dòng)態(tài)庫(kù),這個(gè)動(dòng)態(tài)庫(kù)的加載,仍然屬于加載器的工作范疇,有多少庫(kù)就加載多少庫(kù),但同樣我們也看到,動(dòng)態(tài)庫(kù)的加載量也是有限的,它和用戶棧的增長(zhǎng)方向相反,過(guò)多的庫(kù)會(huì)壓制用戶棧的增長(zhǎng),而用戶棧的無(wú)限擴(kuò)展也會(huì)影響到動(dòng)態(tài)庫(kù)的調(diào)用數(shù)量。
用戶棧是從0xbfffffff開(kāi)始的,那么從0xc0000000開(kāi)始向上,保留的就是內(nèi)核代碼,用于對(duì)所加載程序的控制。
由于C語(yǔ)言的指針訪問(wèn)時(shí)不受程序和操作系統(tǒng)限制的,理論上我們可以訪問(wèn)到空間內(nèi)任意的地址,當(dāng)然,內(nèi)核本身有預(yù)警機(jī)制,當(dāng)你的指針試圖修改代碼段、共享庫(kù)段甚至是內(nèi)核段時(shí),很可能就會(huì)出現(xiàn)著名的“segmentation
fault (core dumped) ”段錯(cuò)誤,這是操作系統(tǒng)的自我保護(hù)機(jī)制。
現(xiàn)在就有個(gè)問(wèn)題可以思考下,我們都知道動(dòng)態(tài)鏈接庫(kù)是在程序運(yùn)行時(shí)才由加載器提供的,我們同樣還知道,鏈接器在生成可執(zhí)行文件時(shí),已經(jīng)把函數(shù)跳轉(zhuǎn)的邏輯地址寫(xiě)死了,那請(qǐng)問(wèn)調(diào)用動(dòng)態(tài)函數(shù)(比如printf)時(shí),由于未執(zhí)行即未加載,匯編代碼是如何解釋printf的跳轉(zhuǎn)地址呢?程序在運(yùn)行時(shí),以什么依據(jù)到上圖中的共享庫(kù)區(qū)域?qū)ふ易约合胍暮瘮?shù)地址呢?
二、位置無(wú)關(guān)代碼PIC
動(dòng)態(tài)庫(kù)存在的一個(gè)主要目的就是,允許多個(gè)正在運(yùn)行的進(jìn)程來(lái)共享相同的庫(kù)代碼,從而節(jié)約寶貴的存儲(chǔ)資源。那么庫(kù)代碼本身在硬盤(pán)的哪個(gè)位置并不重要,只要事先已被編譯,任何進(jìn)程隨時(shí)都可以把它需要的,庫(kù)代碼移花接木到共享庫(kù)映射區(qū)域中。
我們就拿最簡(jiǎn)單的printf函數(shù)來(lái)說(shuō),它是屬于libc.so中的庫(kù)函數(shù),于是我們寫(xiě)個(gè)最簡(jiǎn)單的調(diào)用函數(shù):
#include <stdio.h>
void main(void)
{
printf("haha!\n");
return;
}
直接gcc -O2編譯出來(lái)a.out,于是進(jìn)行反編譯:objdump -D a.out:
真是夠長(zhǎng)的,先看main函數(shù)部分的printf調(diào)用:
08048368 <main>:
8048368: 55 push %ebp
8048369: 89 e5 mov %esp,%ebp
804836b: 83 ec 08 sub $0x8,%esp
804836e: 83 e4 f0 and $0xfffffff0,%esp
8048371: 83 ec 1c sub $0x1c,%esp
8048374: 68 60 84 04 08 push $0x8048460
8048379: e8 22 ff ff ff call 80482a0 <puts@plt>
804837e: c9 leave
804837f: c3 ret
call語(yǔ)句讓我們?nèi)フ?x80482a0地址,好吧,那么我們?nèi)フ艺野l(fā)現(xiàn):
080482a0 <puts@plt>:
80482a0: ff 25 58 95 04 08 jmp *0x8049558
80482a6: 68 00 00 00 00 push $0x0
80482ab: e9 e0 ff ff ff jmp 8048290 <_init+0x18>
好吧,又要跳轉(zhuǎn)到0x8049558,于是我們走著:
反匯編 .got 節(jié):
08049548 <.got>:
8049548: 00 00 add %al,(%eax)
...
反匯編 .got.plt 節(jié):
0804954c <_GLOBAL_OFFSET_TABLE_>:
804954c: 80 94 04 08 00 00 00 adcb $0x0,0x8(%esp,%eax,1)
8049553: 00
8049554: 00 00 add %al,(%eax)
8049556: 00 00 add %al,(%eax)
8049558: a6 cmpsb %es:(%edi),%ds:(%esi)
8049559: 82 (bad)
804955a: 04 08 add $0x8,%al
804955c: b6 82 mov $0x82,%dh
804955e: 04 08 add $0x8,%al
我去……8049558對(duì)應(yīng)的cmpsb語(yǔ)句是,是什么sb東西???接下來(lái)是不是要去百度cmpsb關(guān)鍵字?省了吧,.got節(jié)、.got.plt節(jié)明顯被objdump曲解了。先了解下got和plt到底是什么東西。
.got叫做全局偏移量表(global offset table),而plt是過(guò)程鏈接表(procedure linkage table)。在got表中有g(shù)ot[0]~got[n]的n個(gè)全局量的偏移地址,它符合下表所描述的結(jié)構(gòu)特性:
地址 |
表目 |
內(nèi)容 |
描述 |
|
GOT[0] |
|
.dynamic節(jié)地址 |
|
GOT[1] |
|
鏈接器標(biāo)識(shí)信息 |
|
GOT[2] |
|
動(dòng)態(tài)鏈接器入口點(diǎn) |
|
GOT[3] |
|
printf函數(shù)調(diào)用push地址 |
接著分析,我們先看804954c,它的內(nèi)容80 94 04 08,是不是看起來(lái)很眼熟?對(duì)了,倒過(guò)來(lái)就是地址0x8049480,到這個(gè)地址去看看:
反匯編 .dynamic 節(jié):
08049480 <_DYNAMIC>:
8049480: 01 00 add %eax,(%eax)
8049482: 00 00 add %al,(%eax)
8049484: 24 00 and $0x0,%al
8049486: 00 00 add %al,(%eax)
8049488: 0c 00 or $0x0,%al
804948a: 00 00 add %al,(%eax)
是不是剛好為上表中說(shuō)的.dynamic節(jié)起始地址?!也就是說(shuō)這個(gè)是GOT[0]里存的內(nèi)容,也就是說(shuō)它的長(zhǎng)度是4字節(jié)。依次推下去,如果我們想獲得所關(guān)心的GOT[3]的地址,只需在GOT數(shù)組上跳3個(gè)步進(jìn),用0x804954c加個(gè)3*4字節(jié)即可,于是得到0x8049558,那么GOT[3]里的內(nèi)容拼起來(lái)就是0x80482a6!什么?這個(gè)值怎么得出來(lái)的?是啊,我添加這幾個(gè)字的幾分鐘之前也納悶,自己去年寫(xiě)的東西,當(dāng)初是怎么想出來(lái)的?結(jié)果稍微觀察了下發(fā)現(xiàn),既然GOT數(shù)組是按4字節(jié)對(duì)齊(別問(wèn)我為什么,有本事找glibc開(kāi)發(fā)者問(wèn)去?。?,那么你可以從上表中804955a到8049558地址里面存的值倒著拼回來(lái)不就是0x80482a6了么?唉,看不懂的人一定是大笨蛋!大啊大……笨……egg……
繼續(xù)找這個(gè)地址:
08048290 <puts@plt-0x10>:
8048290: ff 35 50 95 04 08 pushl 0x8049550
8048296: ff 25 54 95 04 08 jmp *0x8049554
804829c: 00 00 add %al,(%eax)
...
080482a0 <puts@plt>:
80482a0: ff 25 58 95 04 08 jmp *0x8049558
80482a6: 68 00 00 00 00 push $0x0
80482ab: e9 e0 ff ff ff jmp 8048290 <_init+0x18>
壓入0,這個(gè)是首個(gè)被調(diào)用的外部函數(shù)所以標(biāo)識(shí)為0,接下來(lái)跳轉(zhuǎn)到8048290,也就是<puts@plt-0x10>: 的部分,壓入0x8049550,這是GOT[1]的地址也就是鏈接器的標(biāo)識(shí)信息。繼續(xù)jmp到0x8049554,這是GOT[2]也就是動(dòng)態(tài)鏈接器入口點(diǎn),這兩個(gè)跳轉(zhuǎn)都是跳到內(nèi)核中執(zhí)行相應(yīng)的代碼,最后動(dòng)態(tài)鏈接器會(huì)通過(guò)一系列變態(tài)運(yùn)算,將printf的地址定位出來(lái),假設(shè)是0x41111111,并用此地址值覆蓋GOT[3]里的值,并把控制傳遞給printf。
當(dāng)下次再調(diào)用printf時(shí),main函數(shù)執(zhí)行call 80482a0 <puts@plt>時(shí),會(huì)繼續(xù)執(zhí)行 80482a0:
ff 25 58 95 04 08 jmp
*0x8049558跳轉(zhuǎn)到GOT[3],但此時(shí)GOT[3]中的0x80482a6已經(jīng)被0x41111111覆蓋,因此程序直接跳轉(zhuǎn)到0x41111111也就是printf庫(kù)函數(shù)地址去執(zhí)行!
我之所以敢隨便編一個(gè)0x41111111,首先因?yàn)槲腋鶕?jù)第一部分所描述的程序進(jìn)程得知?jiǎng)討B(tài)庫(kù)地址從0x40000000開(kāi)始,而具體映射到哪個(gè)值,只有進(jìn)程運(yùn)行后才曉得,所以怎么編都沒(méi)人敢說(shuō)我錯(cuò),哈哈!
回顧上面對(duì)動(dòng)態(tài)函數(shù)的分析,我們發(fā)現(xiàn)這是ELF編譯系統(tǒng)一個(gè)很有趣的技術(shù),他被稱為延遲綁定(lazy
binding),很奇怪為啥不是懶人綁定呢O(∩_∩)O~,意思就是說(shuō),printf的地址綁定不發(fā)生在鏈接器做鏈接時(shí),而是延遲到程序被執(zhí)行,動(dòng)態(tài)鏈接器加載動(dòng)態(tài)庫(kù)后,程序第一次執(zhí)行動(dòng)態(tài)函數(shù)時(shí),才完成函數(shù)地址綁定。