[爆卦]independent of x意思是什麼?優點缺點精華區懶人包

為什麼這篇independent of x意思鄉民發文收入到精華區:因為在independent of x意思這個討論話題中,有許多相關的文章在討論,這篇最有參考價值!作者nowar100 (拋磚引玉)看板C_and_CPP標題[問題] 關於 Position Ind...


遇到的問題: (題意請描述清楚)

本人最近在閱讀某本書,看到動態連結這邊看了老半天,查了一堆資料
卻還是沒辦法完全參透他的意思


在介紹動態連結的時候
他一開始提出的方案為 load time relocation,也就是把重定推遲到載入時才執行
後來書上說
這樣會讓多個行程無法共用該 DSO,沒有達到節省記憶體空間的好處
因此後來出現了 PIC 的概念
這樣可以讓 .text 載入的時候不用重定,而 .data 又可以在不同行程有副本


這幾句話我看了老半天看不懂
1. 為什麼 load time relocation 會造成 DSO 無法被共用?
2. .data 裡面的 GOT,書上說在載入的時候 ld.so 會更新裡面欄位的值,
這跟多行程能否共用該 DSO,在不同行程有副本又有什麼關係?


短短幾頁就讓我無法頓悟,希望對這方面有心得的前輩可以多多幫忙一下
謝謝大家

--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 111.250.174.228
james732:我記得 PIC 是為了要消除絕對位置...? 10/10 15:02

> -------------------------------------------------------------------------- <

作者: littleshan (我要加入劍道社!) 看板: C_and_CPP
標題: Re: [問題] 關於 Position Independent Code 的概念
時間: Sun Oct 10 17:18:19 2010

※ 引述《nowar100 (拋磚引玉)》之銘言:
: 遇到的問題: (題意請描述清楚)
: 本人最近在閱讀某本書,看到動態連結這邊看了老半天,查了一堆資料
: 卻還是沒辦法完全參透他的意思
先猜那本書就是「程式設計師的自我修養」XD

: 在介紹動態連結的時候
: 他一開始提出的方案為 load time relocation,也就是把重定推遲到載入時才執行
: 後來書上說
: 這樣會讓多個行程無法共用該 DSO,沒有達到節省記憶體空間的好處
: 因此後來出現了 PIC 的概念
: 這樣可以讓 .text 載入的時候不用重定,而 .data 又可以在不同行程有副本
: 這幾句話我看了老半天看不懂
: 1. 為什麼 load time relocation 會造成 DSO 無法被共用?
假設有一段 code 是這樣:

int x; // global variable
void inc()
{
++x;
}

compile 的時候,因為 x 的位址無法確定
所以 inc 的指令會像這樣

.code
inc:
mov XXXX, %eax
inc %eax
mov %eax, XXXX

其中 XXXX 代表變數 x 的位址,這個位址要交給 linker 決定後再填入

假設有兩支程式 A 和 B 都會用到 inc()
那麼在執行 A 的時候
ld.so 會進行一次 relocation
並且把 XXXX 代換成 x 真正在記憶體中的位址
所以 inc 這段函式載入到記憶體後可能長這樣:

.code
inc:
mov 0x0004, %eax (這邊的意思是把 0x0004 這個位址的內容放進 %eax)
inc %eax (很久沒寫組語了,語法小錯誤請多見諒)
mov %eax, 0x0004

OK,現在 A 還沒跑玩,使用者又執行了 B 程式
ld.so 又進行第二次 relocation
不幸的是 B 這支程式包含了許多其它模組,因此 ld.so 把 x 分配到 0x2000 這個位址
這麼一來 inc() 會變這樣:

.code
inc:
mov 0x2000, %eax
inc %eax
mov %eax, 0x2000

也就是說,如果不使用 Position independent code
那麼 inc 這個函式編譯出來的碼
與不同的程式連結時,其指令的內容也會跟著不同
這麼一來 inc 這個函式勢必在記憶體中要有兩份副本
一份是給 A 用的 (x位址為 0x0004的版本)
另一份是給 B 用的 (x位址為 0x2000的版本)

: 2. .data 裡面的 GOT,書上說在載入的時候 ld.so 會更新裡面欄位的值,
: 這跟多行程能否共用該 DSO,在不同行程有副本又有什麼關係?
GOT 的目的是為了讓這個 DSO 可以存取另一個 DSO 中的 data
考慮一下上面的例子
如果 x 是定義於另一個 DSO 中的資料
同時我們又希望「存取 x 的指令本身是 position independent」
那方法就是繞一圈:先取得 GOT 的位址 (GOT 對於 DSO 來說算是定義於模組內的資料)
然後再從 GOT 的內容 (由ld.so填入) 去取得 x 的位址

: 短短幾頁就讓我無法頓悟,希望對這方面有心得的前輩可以多多幫忙一下
: 謝謝大家

--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 118.168.80.53
nowar100:1. 那個x不是會放在.data嗎,應該可直接算出相對offset吧 10/10 17:39
nowar100:如果說該DSO並不會用到其他DSO 那PIC是不是就沒有意義了? 10/10 17:40
james732:第一行猜得跟我一樣 XDD 10/10 17:42
loveme00835:@_@ 小弟程度不夠, 進度還沒到那 10/10 17:42
purpose:回完文才看到這篇 10/10 17:44
nowar100:下面好幾篇看起來不錯,等我吃完飯回來研究,先謝謝大家 10/10 17:45
manlike:一樓的問題可以看樓下coldstars大大的文章~ XD 10/10 18:07
manlike:你用相對的offset 就是使用PIC的技巧了 PIC還是有用的 XD 10/10 18:10
yoco315:最近大家都在看這本哩.. XD 10/10 19:13
coldstars:不過我一直有疑惑為啥大家都想看這本XD 10/10 19:46
coldstars:有時間不是應該要專精自己的領域嗎? 10/10 19:47
coldstars:要正確使用的話,其實文件裡都講很清楚了... 10/10 19:48
james732:咦 樓上有更好的選擇嗎?我覺得這本值得推薦耶 10/10 19:48
coldstars:我的意思是...除非這邊板眾都是走系統的 10/10 19:49
coldstars:不然看這個其實很浪費時間...他不會對你的專長有幫助 10/10 19:50
coldstars:這些系統眉角的地方都已經被tune到幾乎符合直覺 10/10 19:50
coldstars:像...我記得有位板友是做CG的,但是他常常問底層實作 10/10 19:51
coldstars:他把時間花在CG上的話價值會更高 (這麼想玩系統快轉行XD 10/10 19:52
james732:也是啦,不過想探究底層的話,這本書幫助確實很大 10/10 19:58
james732:就算是無助專長 多懂一點點也沒什麼不好囉 XD 10/10 19:58
coldstars:書本身的話 在中文書裡的確是近期非常值得推薦的 10/10 20:04
yoco315:像我這種沒專精領域的, 就只好什麼都看了 QQ 10/10 21:10
coldstars:與其說yoco沒有強項不如說是全般萬能吧XD 10/10 21:55

> -------------------------------------------------------------------------- <

作者: purpose (purpose) 看板: C_and_CPP
標題: Re: [問題] 關於 Position Independent Code 的概念
時間: Sun Oct 10 17:43:52 2010

※ 引述《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
manlike:這篇也不錯~ 只是有些地方好像怪怪的?? @@? 10/10 18:12
manlike:而且把 Windows dll 和 Linux so 放在一起講很酷 XD 10/10 18:13
purpose:有些地方小弟只有看看資料,做個人理解的講述,怕誤導大家 10/10 18:17
purpose:在這裡說明一下 10/10 18:17
nowar100:謝謝,不過我還是先研究好DSO再來研究DLL 怕混淆 10/10 19:00

修文,上述內容不變動,新增以下內容。

下面補充一下 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)

> -------------------------------------------------------------------------- <

作者: coldstars (あら~) 看板: C_and_CPP
標題: Re: [問題] 關於 Position Independent Code 的概念
時間: Sun Oct 10 17:44:32 2010

※ 引述《nowar100 (拋磚引玉)》之銘言:
: 遇到的問題: (題意請描述清楚)
: 本人最近在閱讀某本書,看到動態連結這邊看了老半天,查了一堆資料
: 卻還是沒辦法完全參透他的意思
: 在介紹動態連結的時候
: 他一開始提出的方案為 load time relocation,也就是把重定推遲到載入時才執行
: 後來書上說
: 這樣會讓多個行程無法共用該 DSO,沒有達到節省記憶體空間的好處
: 因此後來出現了 PIC 的概念
: 這樣可以讓 .text 載入的時候不用重定,而 .data 又可以在不同行程有副本
: 這幾句話我看了老半天看不懂
: 1. 為什麼 load time relocation 會造成 DSO 無法被共用?

我覺得他寫錯了,或是你看錯...
即使是PIC的image,也是load的時候才做relocation XD
甚至有些東西還是第一次call到的時候才做

能不能省記憶體主要是靠PIC (gcc是用-fpic/-fPIC的option)

reloc是要解決有些address在執行時才知道的問題
通常都是其他module的位址,所以編譯時當然不知道了...
而每個process載入一個DSO的位址未必相同

例如說今天有libz.so需要libc.so好了
libz裡的reloc,會指向libc.so的某個function
然後我們同時跑起兩隻程式: tar和unzip
這兩隻程式都會需要libz.so (假設啦...我也不知道實際上是怎樣XD)
所以libz.so會同時被load到兩個process裡面去
雖然兩個process都包含了libz.so,裡面又包含了指向libc.so的reloc
但他們實際數字可能不太一樣

如果你的reloc遍佈整個DSO的image,
那即使有多process使用這個DSO,OS也不能把同一份DSO image讓所有process共用
因為DSO image裡有太多page都各自copy-on-write而不能直接mmap

-fpic的作用是讓程式裡access external address的部分
通通移到.text外面,這樣至少.text只佔一份空間
因此其實是對OS來說省記憶體,對process來說沒什麼影響

至於那些.got的section就讓他不停duplicate也無所謂XD
大部分DSO這些section都不會很大,所以還好啦

: 2. .data 裡面的 GOT,書上說在載入的時候 ld.so 會更新裡面欄位的值,
: 這跟多行程能否共用該 DSO,在不同行程有副本又有什麼關係?

被改過以後OS就要幫他copy-on-write,不能讓多process共用

.data/.got這些section本來就沒辦法共用
但如果沒有下-fpic的option,那些需要改的地方就會遍佈整個.text
變成連.text都不能共用了

--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 114.42.87.44
manlike:好文~ XD 10/10 18:05

修一下例子看能不能比較清楚...
※ 編輯: coldstars 來自: 114.42.87.44 (10/10 18:32)
nowar100:請問 文中image指的是可執行檔? 還是.text和.data? 還是? 10/10 18:37
coldstars:exe/out/dll/so 這些都是image 就是不是o/lib的那些XD 10/10 18:44
nowar100:所以在使用PIC之後 load需要重訂的只有.data 沒有.text? 10/10 19:00
coldstars:對 需要reloc的都會被塞到.data/.got之類的地方 10/10 19:12

> -------------------------------------------------------------------------- <

作者: nowar100 (拋磚引玉) 看板: C_and_CPP
標題: Re: [問題] 關於 Position Independent Code 的概念
時間: Sun Oct 10 19:15:51 2010

其實我對於您提出的例子,沒辦法理解,以下是我的謬論(?)


假設 libfoo.so 的 Code 如您所說

int x;
void inc() { ++x; }

那麼在編譯及連結之後, x 應該是長在該 DSO 自己的 .data 區
也就是我推文所講的,應該是可以直接從 .data 的 offset 算出要填入的位址
這應該跟 PIC 無關才對吧,照這樣看,還沒載入之前,位址就應該都是固定的吧
這樣即使 A, B 兩行程同時使用 libfoo.so,該 DSO 的 .text 應該也是固定的阿


我以為,只有在 libfoo.so 參考其他 libbar.so 這種情況下
PIC 才會對 load time relocation 有差別吧


感覺還是似懂非懂,麻煩前輩們請多再指教


※ 引述《littleshan (我要加入劍道社!)》之銘言:
: ※ 引述《nowar100 (拋磚引玉)》之銘言:
: : 遇到的問題: (題意請描述清楚)
: : 本人最近在閱讀某本書,看到動態連結這邊看了老半天,查了一堆資料
: : 卻還是沒辦法完全參透他的意思
: 先猜那本書就是「程式設計師的自我修養」XD
: : 在介紹動態連結的時候
: : 他一開始提出的方案為 load time relocation,也就是把重定推遲到載入時才執行
: : 後來書上說
: : 這樣會讓多個行程無法共用該 DSO,沒有達到節省記憶體空間的好處
: : 因此後來出現了 PIC 的概念
: : 這樣可以讓 .text 載入的時候不用重定,而 .data 又可以在不同行程有副本
: : 這幾句話我看了老半天看不懂
: : 1. 為什麼 load time relocation 會造成 DSO 無法被共用?
: 假設有一段 code 是這樣:
: int x; // global variable
: void inc()
: {
: ++x;
: }
: compile 的時候,因為 x 的位址無法確定
: 所以 inc 的指令會像這樣
: .code
: inc:
: mov XXXX, %eax
: inc %eax
: mov %eax, XXXX
: 其中 XXXX 代表變數 x 的位址,這個位址要交給 linker 決定後再填入
: 假設有兩支程式 A 和 B 都會用到 inc()
: 那麼在執行 A 的時候
: ld.so 會進行一次 relocation
: 並且把 XXXX 代換成 x 真正在記憶體中的位址
: 所以 inc 這段函式載入到記憶體後可能長這樣:
: .code
: inc:
: mov 0x0004, %eax (這邊的意思是把 0x0004 這個位址的內容放進 %eax)
: inc %eax (很久沒寫組語了,語法小錯誤請多見諒)
: mov %eax, 0x0004
: OK,現在 A 還沒跑玩,使用者又執行了 B 程式
: ld.so 又進行第二次 relocation
: 不幸的是 B 這支程式包含了許多其它模組,因此 ld.so 把 x 分配到 0x2000 這個位址
: 這麼一來 inc() 會變這樣:
: .code
: inc:
: mov 0x2000, %eax
: inc %eax
: mov %eax, 0x2000
: 也就是說,如果不使用 Position independent code
: 那麼 inc 這個函式編譯出來的碼
: 與不同的程式連結時,其指令的內容也會跟著不同
: 這麼一來 inc 這個函式勢必在記憶體中要有兩份副本
: 一份是給 A 用的 (x位址為 0x0004的版本)
: 另一份是給 B 用的 (x位址為 0x2000的版本)
: : 2. .data 裡面的 GOT,書上說在載入的時候 ld.so 會更新裡面欄位的值,
: : 這跟多行程能否共用該 DSO,在不同行程有副本又有什麼關係?
: GOT 的目的是為了讓這個 DSO 可以存取另一個 DSO 中的 data
: 考慮一下上面的例子
: 如果 x 是定義於另一個 DSO 中的資料
: 同時我們又希望「存取 x 的指令本身是 position independent」
: 那方法就是繞一圈:先取得 GOT 的位址 (GOT 對於 DSO 來說算是定義於模組內的資料)
: 然後再從 GOT 的內容 (由ld.so填入) 去取得 x 的位址
這部分也因為上面沒參透,不懂為什麼 x 的位址需要動態來取得
等第一題我先解決了再來研究

: : 短短幾頁就讓我無法頓悟,希望對這方面有心得的前輩可以多多幫忙一下
: : 謝謝大家

謝謝

--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 111.250.174.228
nowar100:等討論一段落,我把本串收進精華區好了,版上資料好少 10/10 19:18
Chikei:因為不考慮超過一個以上的process同時共用的情況的話, 10/10 22:43
Chikei:不用offset,直接取address會比較快,所以非PIC的情況下, 10/10 22:44
Chikei:那段code會被編成直接取address的code,然後loader在init的 10/10 22:45
Chikei:根據分配的記憶體位址填入。 10/10 22:45

> -------------------------------------------------------------------------- <

作者: littleshan (我要加入劍道社!) 看板: C_and_CPP
標題: Re: [問題] 關於 Position Independent Code 的概念
時間: Mon Oct 11 00:24:33 2010

※ 引述《nowar100 (拋磚引玉)》之銘言:
: 其實我對於您提出的例子,沒辦法理解,以下是我的謬論(?)
: 假設 libfoo.so 的 Code 如您所說
: int x;
: void inc() { ++x; }
: 那麼在編譯及連結之後, x 應該是長在該 DSO 自己的 .data 區
: 也就是我推文所講的,應該是可以直接從 .data 的 offset 算出要填入的位址
: 這應該跟 PIC 無關才對吧,照這樣看,還沒載入之前,位址就應該都是固定的吧
: 這樣即使 A, B 兩行程同時使用 libfoo.so,該 DSO 的 .text 應該也是固定的阿
: 我以為,只有在 libfoo.so 參考其他 libbar.so 這種情況下
: PIC 才會對 load time relocation 有差別吧
: 感覺還是似懂非懂,麻煩前輩們請多再指教
如果你有看過 x86 的 machine code 格式
大概不會有這樣的疑問
比如說 ++x 的命令
在不使用 PIC 的情況下編成 machine code 後會長這樣:
(以下是我編出 .exe 後再用 dumpbin 進行反組譯後的結果)

00401003: A1 C0 BD 40 00 mov eax,dword ptr ds:[0040BDC0h]
00401008: 83 C0 01 add eax,1
0040100B: A3 C0 BD 40 00 mov dword ptr ds:[0040BDC0h],eax

其中 0x0040BDC0 就是 x 的位址
根據 x86 指令集的規格
這個位址必需是絕對位置
也就是 x 真正在 virtual memory space 中的位置
而不是「相對於某個地方的 offset」

所以
只要 libfoo.so 被 ld.so 載入到記憶體中的不同位置
x 的絕對位址就會隨之變動
導致上面的 machine code 必需改寫

--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 118.168.80.53
nowar100:了解了,非常謝謝您的解說 10/11 00:52

> -------------------------------------------------------------------------- <

作者: coldstars (あら~) 看板: C_and_CPP
標題: Re: [問題] 關於 Position Independent Code 的概念
時間: Mon Oct 11 00:48:18 2010

※ 引述《nowar100 (拋磚引玉)》之銘言:
: 其實我對於您提出的例子,沒辦法理解,以下是我的謬論(?)
: 假設 libfoo.so 的 Code 如您所說
: int x;
: void inc() { ++x; }
: 那麼在編譯及連結之後, x 應該是長在該 DSO 自己的 .data 區
: 也就是我推文所講的,應該是可以直接從 .data 的 offset 算出要填入的位址
: 這應該跟 PIC 無關才對吧,照這樣看,還沒載入之前,位址就應該都是固定的吧
: 這樣即使 A, B 兩行程同時使用 libfoo.so,該 DSO 的 .text 應該也是固定的阿
: 我以為,只有在 libfoo.so 參考其他 libbar.so 這種情況下
: PIC 才會對 load time relocation 有差別吧
: 感覺還是似懂非懂,麻煩前輩們請多再指教


但是DSO並不知道自己會被載入到哪個位址
所以位址固定,其實對DSO來說應該是 "在我自己的範圍裡,位址固定"

假設DSO的起點 = base
那這個image大概會長這樣

base:
.data:
// TYPE NAME
int32 x
...
...

.text:
inc:
load [x] -> %r0
add %r0, %r0, 1
store [x] <- %r0
ret

指令請自行想像XD

然後 load x -> %r0 要怎麼知道x的位址呢?
因為編譯時不知道base是多少,
如果不選擇編譯成PIC的話,直接hard-coded進去是最省的
這樣子就違反.text跟位址無關的原則了

編譯成PIC的話,實際上可能得這樣做

_tmp:
move %base -> %r0 // 找出我的base
add %r0, offset(x) -> %r0
load %r0 -> %r0 // 從%r0裡的address讀出值

這部分在各種平台可能有很不同的做法,並非每個平台都有這個%base可用
所以很多平台是用當前的PC來算
畢竟inc()跟x的距離是固定的,加上一個常數就可以找到x

像ARM在data access方面也支援PC-relative addressing mode
他就不需要特地取PC也能直接讀到x的值

x86的話就需要迂迴一點去取PC了

call _this
_this:
pop %eax

這樣就可以把PC騙到%eax裡
(gcc是call進一個小function,然後mov [%esp] -> %ebx)

接著就是 addr(x) = _this - (offset(_this) - offset(x))


以上是static int x的處理方式
跟int x是不一樣的,不過先不要管這麼多好了XD

--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 114.42.87.44
loveme00835:好恐怖的討論串 @_@ 10/11 00:50
nowar100:謝謝您詳細的說明 :) 10/11 00:53
nowar100:本串收錄至 z-3-16-4 10/11 00:53
manlike:可是 DSO 的 .data 在每個 process 的位址都不一樣 10/11 01:35
manlike:所以對ARM而言 PC relative 也是沒用 .. 10/11 01:36
coldstars:但是.data跟.text的距離是不會變的 10/11 01:37
coldstars:所以要在local找東西的話只要不要太遠都可以 10/11 01:39
manlike:那是在DSO裡的offset不會變 當process載入後DSO的.text和 10/11 01:39
manlike:會共用 但是不同的process DSO的.data會load到不同位址.. 10/11 01:41
coldstars:所以ARM的DSO載入後 .text和.data的距離不是固定的? 10/11 01:48
coldstars:這樣的話的確就行不通 10/11 01:48
manlike:DSO的機制跟底層硬體ISA無關 只跟上層OS怎麼做DSO有關 .. 10/11 01:50
manlike:所以DSO的機制才會這麼複雜 用很多table @@" 10/11 01:51
coldstars:ISA還是有影響 ISA很強的話實作各種功能會輕鬆很多 10/11 01:53
coldstars:有些非平民級XD 的機器 所有的code都是PIC 沒有non-PIC 10/11 01:54
littleshan:有virtual memory的話,要讓text與data距離固定不難吧 10/11 04:19
littleshan:老實說我很難想像不使用vm的多工作業系統,現在還有嗎? 10/11 04:24

你可能也想看看

搜尋相關網站