連結器、載入器與目的檔格式

與位置無關的程式碼(position-independent code)

/ PIC, said pick /

一個共享函式庫在不同程式裡會被載入到不同的記憶體位址——它沒有單一固定的家,因為兩個函式庫可能都想要同一個位址。所以共享函式庫裡的程式碼不能寫死絕對位址,像「跳到 0x401000」。與位置無關的程式碼是這樣寫的:無論它被載入到哪個基底位址都能正確執行:每個跳躍、呼叫與資料存取都以相對於目前位置來表達,或透過一張小小的每實例表來轉接,絕不寫成固定的絕對數字。

這招有兩部分。對於觸及程式碼與鄰近資料,PIC 使用 PC 相對定址:指令不是「從絕對位址 0x404000 載入」,而是「從目前指令指標(rip)前方 0x3000 位元組處載入」。因為偏移量是相對的,無論整塊放在哪都保持正確。對於觸及「其他」模組裡的全域變數與函式——它們的位址只在載入時才知道——PIC 加一層間接:它在一張每行程的表(叫全域偏移表,GOT)裡查真正的位址,那張表由動態連結器在啟動時填一次。所以程式碼本身不含任何模組特定的絕對位址;只有 GOT 含,而 GOT 是唯一需要載入期修正的東西。你用旗標 -fPIC 向編譯器要求這個。

它重要,是因為 PIC 讓一份共享函式庫的程式碼頁能在每個使用它的行程間被共享:程式碼相同且唯讀,所以核心到處映射同一批實體頁,只有那小小的可寫 GOT 每行程不同。代價是一點間接與略大的程式碼。一個常見誤解是 PIC 表示「完全沒有重定位」——它表示「少」重定位,集中在 GOT,而非散布在每條指令裡。在 64 位元 x86 上 PC 相對定址很便宜,這也是與位置無關的可執行檔成為預設的部分原因。

$ gcc -fPIC -shared -o libmath.so math.c # 觸及區域變數,PC 相對(無絕對位址): lea rax, [rip + 0x2f01] ; 鄰近某全域的位址 # 觸及外部符號,透過 GOT: mov rax, [rip + foo@GOTPCREL]

PIC 以 rip 相對定址觸及鄰近資料,並透過 GOT 間接觸及其他模組的符號,所以沒有絕對位址被烤進程式碼裡。

PIC 不消除重定位;它把它們集中在 GOT,讓程式碼頁本身保持與位址無關、可共享。Linux 上的共享函式庫基本上必須是 PIC;非 PIC 的 .so 需要 text relocation,那會破壞程式碼共享,且被現代工具鏈禁止。

又稱
PICposition-independent code與位置無關的程式碼