轉(zhuǎn)自: http://ucos./blog-1403-3083.html
read-only pages: read-write pages: .bss 一、文件的加載 這一過程有點長,但很直觀。 首先:啟動動態(tài)鏈接器 一個程序啟動之后,系統(tǒng)先把文件內(nèi)容映射到內(nèi)存中,然后..... 首先,系統(tǒng)通過.interp節(jié)找到動態(tài)鏈接器,并將其加載到內(nèi)存空間,再啟動鏈接器。還要把一個輻助向量傳到動態(tài)鏈接器的??臻g里: AT_PHDR, AT_PHENT, and AT_PHNUM:程序頭表的地址,每個表項的大小和表項數(shù)。 AT_ENTRY: 程序的入口地址 AT_BASE: 動態(tài)鏈接器加載的基地址 然后,動態(tài)鏈接器找到自己的.got節(jié),其中的第一條目指向動態(tài)鏈接器自己的.dynamic節(jié)。然后動態(tài)鏈接器先重定位自己的變量和必要函數(shù)。使自己可用。(動態(tài)鏈接器ld.so的重要函數(shù)都以_dt_開頭,一段特殊的代碼會直接找到這種字符串開頭的符號,并解析它們) 最后,動態(tài)鏈接器初始化一系統(tǒng)的符號表,其中有指針指向動態(tài)鏈接器自己的符號表和程序的符號表。從概念上來講,一個進(jìn)行的程序文件和所有的庫是共享一個符號表的。但是鏈接器不沒有把所有的符號表混合在一起,而是用一個鏈表把他們串起來。每個文件都有自己的hash表。鏈接器查找符號時只計算一次哈希值,然后逐個哈希表去匹配。 然后找到需要的庫 動態(tài)鏈表器自己的初始化完成之后,它就開始為程序加載庫文件。程序的program header table有一個條目指向程序的DYNAMIC段,也即.dynamic節(jié)。它里面包含的是動態(tài)鏈接信息。包含一個DT_STRTAB項,指向.dynstr節(jié),和多個ST_NEEDED項,是需要的庫,其名字指向字符號表(.dynstr)中的偏移: 例子: readelf -d test得到的.dynamic節(jié)條目表 readefl -S test得到的節(jié)頭表 找到需要的庫名字之后,動態(tài)鏈接器就會去系統(tǒng)里找相同名字的庫文件,這是一個相當(dāng)復(fù)雜的過程。比如說在上面的例子中,需要的庫文件是libc.so.6(C庫,版本6),找它的過程需要去系統(tǒng)的庫路徑個逐個搜索??赡苷业降膸烀忠膊煌耆粯?,比如說可能會找到lib.so.6.2,即后面還帶了更小的版本號。 鏈接器查找?guī)煳募奈恢冒ㄈ缦聨滋帲?/span>
最終,動態(tài)鏈接器找到了所有的庫,并且擁有了一個邏輯上的全局符號表。 初始化共享庫 動態(tài)鏈接器重新訪問每個庫,處理它的重定位條目,添寫其GOT(全局偏移表),并對數(shù)據(jù)區(qū)的符號進(jìn)行重定位。加載階段的重定義包括:
惰型過程鏈接(原名是lazy procedure linkage,沒找到通用的翻譯方法,自己取的名字) 為什么說是惰型的呢?因為這種鏈接方式很拖拉,不到用時不鏈接。過程鏈接是說這種鏈接是面向函數(shù)的。 有些函數(shù),在程序運行過程中很少會調(diào)用,比如說錯誤處理什么的。函數(shù)依賴的庫還會依賴其他庫,但依賴的其它庫的函數(shù)也很少會用到。如果所有的都加載的話,可能你系統(tǒng)里的所有庫都必須加載到內(nèi)存中去。那機(jī)器就沒法跑了。 所以對于不常用的函數(shù),我們在不用他的時候,就不加載他所有的庫。用時再說。 這樣也能加快程序啟動時間,很多庫都不用加載了嘛。 但那些沒有解析的符號怎么辦?先不管,留著用到時再綁定。但這些符號可不能放到原來的地方了,我們需要另外的處理方法,和存儲方法。 存儲方法叫Procedure Linkage Table.PLT。每個動態(tài)綁定的程序或庫都有一個plt表。里面包含的條目是程序或庫調(diào)用的非本地例程。當(dāng)程序或庫調(diào)用到這個例程的時候,都會跳入到plt的相應(yīng)條目中去,相應(yīng)的條目都是幾條簡單的指令,如果這個函數(shù)例程還沒有綁定,就是調(diào)用動態(tài)鏈接器進(jìn)行綁定。如是不是第一次調(diào)用,那么函數(shù)就己經(jīng)綁定了,就是直接跳轉(zhuǎn)到函數(shù)中去。
PLT中的第一個條目叫做PLT0,它的代碼與其他PLTn不同。PLT0就是那個調(diào)用動態(tài)鏈接器的代碼。但第一次調(diào)用某個函數(shù)時,PLTn都會跳到PLT0上來,然后用PLT0的代碼來調(diào)用動態(tài)鏈接器。 在程序裝載階段,動態(tài)鏈接器會在GOT中自動加上兩個條目:在GOT的第二個字上,和第三個字上,即GOT+4和GOT+8。依次放入的是庫鑒定代碼(這個代碼可以知道當(dāng)前要調(diào)用一個未解析的符號的程序或庫是什么)和動態(tài)鏈接器的符號解析函數(shù)地址。每個PLTn條目第一個指令都是一個跳轉(zhuǎn)指令,跳轉(zhuǎn)到*GOT+m上去。每個PLTn條目都對應(yīng)著一個GOT上的條目,那個GOT上的條目初始化時是一個指向這個PLT上跳轉(zhuǎn)指令的下一條指令,是push指令,也就是說開始時是PLTn跳到GOT+m后馬上跳回來執(zhí)行下面的push指令(這塊可以看看上面的指令圖)。push指令就是push #reloc_offset,它入棧的是重定向表中的一個條目,這個條目的類型為 R_386_JMP_SLOT。這個條目的目地地址是符號表中一個符號,這個符號表條目的value項正指向GOT+m,也就是說現(xiàn)將要解析的符號解析為GOT+m. 這種安排很詭怪。但也很巧妙。當(dāng)?shù)谝淮芜M(jìn)入PLTn時,第一條指令跳到GOT+m后什么也沒做就返回來接著執(zhí)行push指令了。這個push指令將要解析的符號的重定位表上的條目偏移入棧。它的目的地址是符號表中的條目,條目的目的地址則是GOT+m,下一步我們把解析出來的真正地址填到GOT+m就可以了。 PLTn的第三條指令就是jump PLT0, PLT0: pushl GOT+4 jmp *GOT+8這里將鑒定程序或庫的代碼入棧,然后調(diào)用GOT+8指定的符號解析例程。 這里有一點東西要多說一下: 當(dāng)我們調(diào)用一個call指令的時候,它將eip入棧,然后跳到調(diào)用的函數(shù)里,函數(shù)返回時將eip出棧,接著執(zhí)行eip所指位置的指令。 在這里也用了這個小技巧,這里先入棧的是#reloc_offset,然后入棧的是GOT+4,然后jump到符號解析例程。 想想:符號解析例程返回時會有什么操作?就是出棧一個數(shù),然后按這個數(shù)指定的位置執(zhí)行指令,也就是說這里GOT+4里的內(nèi)容會pop eip。 這樣,進(jìn)入符號解析例程之后,符號解析例程在鏈在一起的那些符號表里找到要解析的符號,然把符號地址存到GOT+m里,返回。然后先pop出庫簽定例程,確定是哪個程序做的調(diào)用,然再pop出重定位表,去重定位表中重新調(diào)用那個函數(shù),這時再進(jìn)入PLTn時,就直接接跳到GOT+m到跳到相應(yīng)的函數(shù)上去了,不會再執(zhí)行PLTn中的下兩條指令了。
|
|