最近為了理解elf格式規(guī)范中的各種重定位類型,暈了。跑出去玩了幾天,終于為每種重定位類型,找到了對(duì)應(yīng)的case。elf規(guī)范總共定義了10種重定位類型,之所以需要這么多種不同類型的重定位信息,是由于如下原因:
① 硬件對(duì)變量和函數(shù)的尋址方式不同,尋找變量要求絕對(duì)地址,尋找函數(shù)要求相對(duì)地址;
② 不同場(chǎng)合下,程序員對(duì)最終可執(zhí)行文件或動(dòng)態(tài)庫的期望不一樣(位置無關(guān)、動(dòng)態(tài)庫函數(shù)重定位延遲),從而加了不同的編譯選項(xiàng)(比如-fPIC、-Ox等);
③ C語言的static、extern特性,導(dǎo)致不同特性的變量或函數(shù)地址可以被確定的時(shí)機(jī)不同;
④ 內(nèi)核加載可執(zhí)行文件,約定從固定地址0x80480000開始,但加載.so的起始地址無法約定(一個(gè)可執(zhí)行程序只有一個(gè)main(),但可能依賴多個(gè)動(dòng)態(tài)庫)。
疑問:那整個(gè)系統(tǒng)中,可執(zhí)行程序也不只一個(gè)呀,都約定從相同的起始地址加載,不會(huì)沖突嗎?
因?yàn)槊總(gè)進(jìn)程訪問的都是虛擬地址,由內(nèi)核在背后負(fù)責(zé)將不同進(jìn)程的相同虛擬地址,映射到不同的實(shí)際物理地址(屬于內(nèi)核范疇,不理解沒關(guān)系,不影響對(duì)本貼關(guān)鍵內(nèi)容的理解)。
靜態(tài)鏈接/動(dòng)態(tài)鏈接簡單理解
.c文件中的代碼最終被執(zhí)行,需要經(jīng)歷如下過程:
① 編譯:詞法解析 → 語法解析 → 靜態(tài)鏈接
② 加載:加載可執(zhí)行文件 → 可執(zhí)行文件啟動(dòng)或執(zhí)行時(shí),加載依賴的.so文件 → 動(dòng)態(tài)鏈接
本帖僅關(guān)注靜態(tài)鏈接、動(dòng)態(tài)鏈接過程,靜態(tài)鏈接與動(dòng)態(tài)鏈接區(qū)別:
① 靜態(tài)鏈接處于將1個(gè)或多個(gè).o文件“拼湊”成可執(zhí)行文件階段,處理對(duì)象是文件,文件中的代碼區(qū)沒有只讀屬性,鏈接過程中可以直接修改;動(dòng)態(tài)鏈接處于可執(zhí)行文件或.so文件已被加載到內(nèi)存階段,處理對(duì)象是內(nèi)存,內(nèi)核為代碼區(qū)所在的內(nèi)存區(qū)域設(shè)置了只讀屬性,如果代碼區(qū)有內(nèi)容需要重定位,需要在編譯或靜態(tài)鏈接時(shí),事先準(zhǔn)備一個(gè)間接位置(加載到內(nèi)存不會(huì)被設(shè)置只讀屬性),動(dòng)態(tài)鏈接是對(duì)該間接位置進(jìn)行重定位。
② 通過下圖可以看出,靜態(tài)鏈接將.o的各個(gè)節(jié)“撕開”,屬性相同的節(jié)“拼湊”為可執(zhí)行文件的段;動(dòng)態(tài)鏈接是將“整個(gè)”.so文件安排在與可執(zhí)行文件鏡像相獨(dú)立的位置(圖中最簡化了.o、.so、可執(zhí)行文件的內(nèi)容,用于說明靜態(tài)鏈接與動(dòng)態(tài)鏈接的區(qū)別,它們的內(nèi)容遠(yuǎn)遠(yuǎn)不止.data、.text)。
另外,.so文件還涉及到位置無關(guān)(-fPIC)、延遲加載的選擇(應(yīng)該是跟優(yōu)化級(jí)別有關(guān)),接下來即將詳細(xì)總結(jié)。
主要利用兩個(gè)技巧:
① 在程序編寫階段,雖然不知道以下兩條指令真正執(zhí)行后ebx寄存會(huì)得到什么值,但能確定它的含義是當(dāng)時(shí)eip寄存器的值,那么跟這條指令相對(duì)位置固定的運(yùn)行時(shí)地址,在邏輯上都能在編譯階段“獲知”:
call L1
L1: pop ebx
② 那么,在.so文件中相對(duì)于指令區(qū)域確定位置生成一個(gè).got表,.so被執(zhí)行時(shí).got表的絕對(duì)地址也是可以“獲知”的。這樣,就可以用.got表項(xiàng)的絕對(duì)地址,覆蓋原本在指令區(qū)域的重定位處,而.got表中存放將來才能確定的最終重定位的符號(hào)地址。
① 假設(shè)進(jìn)程A先將libc.so映射到自己的一塊虛擬空間,當(dāng)首次訪問這塊區(qū)間時(shí)發(fā)生缺頁異常,分配物理頁面并讀入內(nèi)容,然后建立映射。接著,進(jìn)程B也將libc.so映射到自己的一塊虛擬空間,首次訪問這塊區(qū)間仍然會(huì)發(fā)生缺頁異常,但與其建立映射的物理頁面,就不用再重新分配讀入了。從而,物理內(nèi)存只需要一份.so的內(nèi)容,就可以供A、B兩個(gè)進(jìn)程使用。
② 思維敏銳的可能會(huì)發(fā)現(xiàn)一個(gè)問題:.so文件中如果有全局變量,被多個(gè)進(jìn)程共享,不是會(huì)相互干擾嗎?
COW(寫時(shí)復(fù)制):內(nèi)核為虛擬頁面、物理頁面都設(shè)置了一些屬性,比如如果對(duì)某個(gè)虛擬頁面進(jìn)行寫操作,就重新分配一個(gè)物理頁面,復(fù)制內(nèi)容并重新建立映射(為.so數(shù)據(jù)區(qū)分配的頁面,就具有這樣的屬性)。
③ 各個(gè)進(jìn)程將.so文件映射到自己的虛擬空間,數(shù)據(jù)區(qū)、代碼區(qū)的相對(duì)位置,仍然保持和剛鏈接過后一致,所以在代碼區(qū)向.got的重定位計(jì)算仍然有效,只不過動(dòng)態(tài)鏈接器為不同進(jìn)程向.got表初始化全局變量的地址時(shí),要向.got表進(jìn)行寫操作,導(dǎo)致每個(gè)進(jìn)程有一個(gè).got副本。
6、b處兩條指令執(zhí)行后,ecx寄存器會(huì)得到.got表加載地址,為什么?
① 前面已經(jīng)說明過R_386_PC32重定位類型,7處經(jīng)過這種類型重定位后,執(zhí)行時(shí)會(huì)跳轉(zhuǎn)到__x86.get_pc_thunk.cx,得到b處指令的加載地址(CPU沒有提供直接獲取當(dāng)前ip的指令,所以利用call會(huì)將返回地址壓棧的特點(diǎn));
② R_386_GOTPC,提示鏈接器創(chuàng)建.got表,并修改d處的值,保證執(zhí)行時(shí)用它加ecx寄存器可以得到.got表地址(可以通過R_386_GLOB_DAT類型分析過程,編譯得到的.so驗(yàn)證):
通過①可能確定,執(zhí)行過6處指令后ecx得到的b處指令的加載地址,拿什么和它相加可以得到.got表位置呢?
+A:從ecx所指位置往后推2字節(jié)(機(jī)器碼“81 c1”),就到了被重定位處(重定位項(xiàng)中的offset/規(guī)范文檔中的P);
+G-P:再向后推.got表相對(duì)此處的距離,就到.got表了。 注意:$0x2只是作為鏈接器計(jì)算重定位值的A,在執(zhí)行時(shí)就被G-P-2覆蓋了,不要疑惑為什么要從ecx減2,它的含義根本就不是減數(shù)。
① 532、537處(對(duì)應(yīng).o文件中6、b處)指令,確實(shí)可以將.got表位置計(jì)算到ecx寄存器中(不過是結(jié)束位置,后面指令取.got表項(xiàng)地址時(shí),用的是負(fù)偏移,可能不同編譯器不一樣吧,用開始位置、結(jié)束位置計(jì)算,道理是一樣的);
② g1、g2的重定位類型變成R_386_GLOB_DAT,它是用于告訴動(dòng)態(tài)鏈接器,在確定g1、g2地址時(shí),放到它們的.got表項(xiàng)里(0x1fe8、0x1ff4)。