Re: [問題] 關於 Position Independent Code 的概念

看板C_and_CPP作者 (purpose)時間13年前 (2010/10/10 17:43), 編輯推噓3(305)
留言8則, 5人參與, 最新討論串3/7 (看更多)
※ 引述《nowar100 (拋磚引玉)》之銘言: : 遇到的問題: (題意請描述清楚) : 本人最近在閱讀某本書,看到動態連結這邊看了老半天,查了一堆資料 程式設計師的自我修養?我買回來只有翻馬上想看的部份,查查資料 還沒看完,有機會可以在版上多討論討論。 : 卻還是沒辦法完全參透他的意思 : 在介紹動態連結的時候 : 他一開始提出的方案為 load time relocation,也就是把重定推遲到載入時才執行 先提一下,Windows 的 .exe (執行檔,即PE格式)、.dll (動態函式庫), 就是用「load time relocation」這個方法, 所以拿來跟 Linux 的 ELF (執行檔)、SO (動態函式庫) 對比很適合。 : 後來書上說 : 這樣會讓多個行程無法共用該 DSO,沒有達到節省記憶體空間的好處 : 因此後來出現了 PIC 的概念 : 這樣可以讓 .text 載入的時候不用重定,而 .data 又可以在不同行程有副本 : 這幾句話我看了老半天看不懂 : 1. 為什麼 load time relocation 會造成 DSO 無法被共用? Windows 的 PE 格式,可以說有四大表格,即資源、匯入、匯出、重定位。 如果某執行檔呼叫了一個 MessageBox() 函數,可以推得就會有一個 call MessageBox 指令。在產生執行檔時,因為 MessageBox 是位於 user32.dll 裡面, 所以 PE 會先用一個假位址替代。然後在匯入表裡面,去放置如何從 user32.dll 取得 MessageBox 位址的資訊。 當你點兩下這個 .exe 開始執行,此時 Windows 的 Loader 把他載入到記憶體裡面, 接著 OS 從此執行檔的匯入表得知有用到 user32.dll,於是就把 user32.dll 這個模組 載入此執行檔的 address space 裡。 假設 user32.dll 是被載入到 0x10203040 位址裡,則原執行檔的 .text 裡面所有 呼叫 call MessageBox 的地方,此時才被修改至真正的位址,也就是 load time relocation。 那 Windows 的 dll 跟 Linux 的 dso (*.so) 相比,並不能達到所謂的「節省記憶體 空間的好處」,因為 Linux 有所謂的 PIC,而 Windows 沒有。 假設 user32.dll!MessageBox 這個函數,其實只是一個空殼,真正的程式碼放在 user32.dll!realBox 裡,則又需要有個 call realBox 指令。 對於 Linux 來說這個 realBox 符號,可以是使用相對位址的 JUMP,比如跳到上方 距離 1000 位元組處; 因此不管有幾個執行檔呼叫了 MessageBox 進而需要跳到 realBox 都不需要 更改 user32 動態函式庫的 .text ,那麼一個 user32.so 就可以給多個行程使用。 而 Windows 比較單純 (原始?),上面提到有個 PE 四大表,而 .dll 也是用 PE 格式, 故也有重定位表。 在 user32.dll!MessageBox 裡的那行 call realBox 打從一開始就是絕對位址, 也就是『與位址相關的程式碼』。 當 user32.dll 作為模組,被某個行程載入進去後,因為每個執行檔可能要載入的模組 數量不一定,順序也不一定,所以 user32.dll 可能在 1.exe 被載入到 0x10203040, 而在 2.exe 卻是被載入到 0x10607080。 因為這不同的載入位址,所以導致 user32.dll!realBox 最終的絕對位址是不同的。 在 user32.dll 的重定位表裡面,就會記錄說在它自己的那些機器碼裡 (.text), 其中第 n 行的 call realBox 需要重定位,且第 k 行也用到 call realBox 指令 也需要重定位。當某執行檔載入 user32.dll 模組時,系統就會先到 user32.dll 的 .text 裡的這些地方去修改位址。(原諒我表達能力不好) 亦即 user32.dll 的 .text 會在 1.exe 跟 2.exe 各自有個副本 → 內容不一樣的副本。 那其實 .exe 也有重定位表,但因為 .exe 在預設狀況下,似乎都是設計成載入到 0x00400000 位址。所以 VC 在編譯時,遇到 1.exe!FunctionDoSomething 就不需要在 重定位表裡,記錄哪幾行有用到 call FunctionDoSomething 的位址,需要被重定位。 他很直接寫 call 0x00405678 就好。此 .exe 被載入記憶體時,這行指令完全不需要 修改,直接就拿來用。 -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 124.8.134.197

10/10 18:12, , 1F
這篇也不錯~ 只是有些地方好像怪怪的?? @@?
10/10 18:12, 1F

10/10 18:13, , 2F
而且把 Windows dll 和 Linux so 放在一起講很酷 XD
10/10 18:13, 2F

10/10 18:17, , 3F
有些地方小弟只有看看資料,做個人理解的講述,怕誤導大家
10/10 18:17, 3F

10/10 18:17, , 4F
在這裡說明一下
10/10 18:17, 4F

10/10 19:00, , 5F
謝謝,不過我還是先研究好DSO再來研究DLL 怕混淆
10/10 19:00, 5F
修文,上述內容不變動,新增以下內容。 下面補充一下 PE 四大表裡的匯入表的前世今生, 主要提供大方向觀念用,細節請翻資料,並動手驗證我講的內容正確性。 首先我們寫了個程式碼 my.c, 將 my.c 編譯後,會產生 my.obj (目的檔;如果用 gcc 就是 my.o), 再將其連結後產生的執行檔就是 my.exe。 my.exe 是一個所謂『PE 格式』的執行檔, 更進一步說,目前 Windows 的 執行檔 (*.exe)、動態連結檔 (*.dll) 都是PE格式。 PE 格式內部可以分成好幾個區塊,其中比較重要的區塊有 .text 放置的是機器碼,比如 mov eax, 3 add eax, 4 call MessageBox 匯出表 如果你建置的 PE 中,有 __declspec(dllexport) void foo (void) 這種要匯出的函數時,就會有此表。 因此我們可以利用 Dependency Walker 查看此表,來得知這個 dll 有哪些匯出 函數。 不是 .dll 裡的所有函數都會匯出給人用,比如 RegisterServiceProcess() 這個函數 google 一下會發現很多人在用,但是你是找不到匯出資訊的。 只能用 LoadLibrary() 動態載入 kernel32.dll 後,再用 GetProcAddress 取得其函數位址。 資源表 重定位表 不重要。 匯入表 功能是幫 call MessageBox 這樣的指令,在載入到記憶體後, 能夠被轉換成正確的位址。 如果用一些偵錯軟體,比如 OllyDbg 開 my.exe 做偵錯,然後按 Alt+M 就可以看到 記憶體分配圖。可以看到 my.exe 裡的 .text、匯入表等資訊,被映射到記憶體位址 0x00400000之後。而且開始運行程式後,按個暫停,再次看記憶體分配圖 (memory map) 多數都會有載入 kernel32.dll,他一樣是 .text 被載入到記憶體裡。 簡單講 .exe 跟 .dll 都算是映像檔 (image),因為執行時關鍵部份都被映射到記憶體, 而目的檔 .obj 就不是映射檔。 回到 my.c,假定其原始碼為 int main() { MessageBox(參數隨便); functionForMy(); return 0; } int functionForMy(void) { ; } 編譯器翻譯成機器碼時,會有 call MessageBox call functionForMy 編譯器一開始就認定 my.exe 將會被載入到 0x0040000,所以 functionForMy 的記憶體 位址,現在可以直接給定,比如說是 call 0x00415678。 那編譯器找不到 MessageBox 的絕對位址,他要怎麼知道這不是你打錯字,而是真的有 這個函數,只不過是透過「隱式連結」。 所以通常需要一行 #pragma comment(lib, "user32.lib") 的指令,告知連結器說 有個匯入用程式庫,裡面還有記載額外的函數資訊。 透過這個檔案知道 MessageBox 確實是個「隱式連結」的函數,不必回報錯誤,讓你重 寫程式。那麼在產生 my.exe 的時候,編譯器就會知道要填寫一個匯入表,裡面用到 一個模組叫 user32.dll,而且是只有用到其中一個函數,叫 MessageBox,簡單記為 user32!MessageBox。 匯入表其實由很多資料結構組成,可以講說由四組陣列在記錄匯入資訊。 其中有一組陣列,裡面每個元素的資料型態叫做「IMAGE_THUNK_DATA」, 這個資料型態名稱你可以把他當放屁,真的! 只要記住每個元素大小是「四位元組」就好, 因為這「四位元組」是 union,在不同情況下有不同意義,扮演四種角色。 我們首先需要知道,實際上 my.exe 的 call MessageBox 指令其實在編譯成 .exe 後 就固定不會改變了,比如寫 call dword ptr[0x00479100],那 0x00479100 這個位址 是什麼?他就像函數指標一樣,會放置 MessageBox 最終的絕對位址。 執行 call MessageBox 就等於,到 0x00479100 取出四個位元組內容,將其當作位址去 call。 這裡講到的 0x00479100 其實就是「匯入表」那四組陣列剛剛我提到的那一組, 每個元素都是「四位元組」的陣列。這個陣列又叫做 IAT (Import Address Table)。 當 user32.dll 作為模組載入到 my.exe 時,Windows 還會順便去填寫 my.exe 的 IAT 每一個元素,我們假設說 IAT[0] 剛好對應 user32!MessageBox 的絕對位址 0x10203040。那必要 my.exe 一開始就知道 Windows 一定會把 IAT[0] 當作 user32!MessageBox 位址的存放處。才能把 call MessageBox 翻譯成 call dword ptr[ IAT[0] ]。 所以其實匯入表四大陣列裡面,又有另外一組陣列叫 INT (Import Name Address) 表。 INT 陣列的每個元素,其資料型態都是『IMAGE_THUNK_DATA』,換言之又是每個元素都是 「四位元組」大小。 可以想成 char *name = "MessageBox"; INT[0] = name; 那只要一開始由編譯器出面,在 my.exe 的匯入表裡面規定說,INT[0] 是代表 user32!MessageBox 函數,就能讓「my.exe 的 .text」跟 Windows Loader 有共同的 認知。我知道要去 IAT[0] 取位址,你也知道要替我把 user32!MessageBox 的最終位址 填進去此處等我拿。 對於 user32!MessageBox,要使用他不是一定要用「隱式連結」,如果用顯式 連結時,是先 LoadLibrary("user32.dll"); 再 GetProcAddree(); 來取得最終位址。 可是在 GetProcAddress() 裡面,可以是一個 "MessageBox" 字串指標,也可以是一個 序數值。只要值介於 0x0000 0000 ~ 0x0000 FFFF 就代表序數; 而 "MessageBox" 對應的字串指標在生成時,他的指標值絕對不會低於 0x0001 0000 所以不會衝突。 剛剛講的隱式連結,只講到 INT 是記錄名稱的「字串指標」,實際上不對, INT 也有可能是記錄像 user32!MessageBox 的序數值 (0x1DD)。 當 INT 陣列的元素,比如 INT[0] 的最高位元為 1 時,代表 INT[0] 是一個序數值。 在 user32.dll 的匯出表裡面,會記錄 user32.dll!MessageBox 的序數是 0x1DD, 所以如果我在 my.exe 的 INT[0] 看到 0x8000 01DD 我就知道他是 user32!MessageBox。 那為什麼 char *name = "MessageBox"; ↑這裡面,得到字串指標不會 >= 0x8000 0000 似乎超過這個位址是 kernel mode 才能用?像記錄每個行程資訊的 EPROCESS 結構 就是在 0x8xxx xxxx 一帶,可用 http://memoryhacking.com 觀看這一帶位址內容。 INT[0] 這四個位元組,可以記載「MessageBox 名稱」或「MessageBox 序數」,實際上 當記錄的是名稱時,他的指標『沒有直接指向字串陣列』! 而是指向「匯入表四大陣列」的其中一個陣列。 這個陣列之前還沒講過,他每個元素的大小是不一定的... 這些元素的資料型態名稱叫 IMAGE_BY_NAME。 前兩個 Bytes 是 Hint,記錄建議序數值用,不用管他。 從第三個 Byte 開始,是 C-Style 字串,內容就是 char str[不一定] = "MessageBox"; 簡單來說 INT[0] 的內容如果是 0x00420000,這個指標會指向 IMAGE_BY_NAME 元素。 你不用管他,直接 +3 變成 0x00420003 這個位址就會放置 "MessageBox"。 而匯入表四大陣列的最後一個,他是提供大綱用的,每個元素的資料結構叫 IMAGE_IMPORT_DESCRIPTOR (IID),有 20 個 Bytes 大。 姑且叫最後這個 IID 陣列叫「匯入大綱表」。 如果 my.exe 有用到 user32.dll 跟 kernel32.dll,那麼 IID陣列 就有兩個元素, char *DLL_NAME1 = "user32.dll"; char *DLL_NAME2 = "kernel32.dll"; IID[0].name = DLL_NAME1; IID[1].name = DLL_NAME2; IID[0].originalFirstThunk = 指標 = INT陣列開頭位址 IID[0].FirstThunk = 還是指標 = IAT陣列開頭位址 IID 陣列的目的是給 Windows Loader 看的,當 my.exe 被載入記憶體執行, Loader 先到匯入表的 IID 陣列知道有兩個模組要載入到 my.exe 的 address space。 當 Loader 載入完後,假設先處理 user32.dll,那就先從 IID[0] 的成員開始處理。 從 IID[0].originalFirstThunk 知道有「user32 專用的 INT 陣列」。 從 IID[0].FirstThunk 知道有「user32 專用的 IAT 陣列」。 從 INT 陣列去知道,只有用到 MessageBox 函數,因此 Loader 去 user32.dll 找出 MessageBox 的最終位址,然後再從對應的 IAT 陣列填寫之。 這樣就完成了 dll 函數的載入時重置工作。 而 my.exe 的 call MessageBox 其實精確說是不用載入時重定位的。 ※ 編輯: purpose 來自: 124.8.134.197 (10/10 21:57)

10/10 22:07, , 6F
INT[0] 應該是 +2 變成 0x00420002,上文打錯了。
10/10 22:07, 6F

10/10 22:45, , 7F
看到頭昏了, 先推再慢慢消化XD
10/10 22:45, 7F

10/11 07:39, , 8F
先推囉,消化這篇需要時間 XD
10/11 07:39, 8F
文章代碼(AID): #1CiOhQma (C_and_CPP)
討論串 (同標題文章)
文章代碼(AID): #1CiOhQma (C_and_CPP)