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

重定位(a relocation)

當編譯器把一個原始檔組譯成 .o 時,它為像 foo() 這樣的呼叫寫出機器碼,但還不能填入 foo 的位址,因為 foo 住在另一個尚未被放進記憶體的檔案裡。所以它留下一個洞,並寫一張便條說「這裡,在這個偏移量,補上 foo 的位址」。那張便條就是重定位。重定位是給後續階段的指示,要在真正位址已知後,修正程式碼或資料中某個特定位置。

每個重定位項目記錄三件事:偏移量(節區中洞的位置)、符號(誰的位址要放進去,例如 foo)、以及一個型別,精確說明如何算出該值——例如寫入絕對的 64 位元位址,或寫入相對於目前指令的 32 位元差值(PC 相對),可能再加上一個加數(addend)。ELF 把這些放在以其目標命名的節區裡,例如 .rela.text 放修補 .text 的重定位。連結器在連結時處理它們:既然它已決定每個節區與符號要坐落何處,它就拜訪每個重定位、算出最終值、寫進那個洞。重定位型別是依架構而定的(你會看到像 R_X86_64_PC32 或 R_X86_64_GLOB_DAT 的名字)。

它重要,是因為重定位是把一堆各自獨立編譯、每個都以為自己從位址零開始的片段,黏合成一個一切都指向正確位置之程式的膠水。這件事可能在兩個時間點發生,而這個區別是核心:連結期重定位由連結器在建構可執行檔時做一次,載入期重定位則由動態連結器在程式每次啟動時做,針對那些只有在函式庫被映射到實際載入位址後才知道的位址。與位置無關的程式碼被設計成只需極少的載入期重定位,這就是它能被便宜地共享的原因;一個必須載入到固定位址的非 PIC 二進位檔在執行期不需要任何重定位,但無法像 PIC 那樣靈活地重定位或共享。

$ readelf -r call.o Offset Info Type Sym. Name + Addend 00000000001b 000d00000004 R_X86_64_PLT32 foo - 4 # '在位元組 0x1b 處,修補一個對 foo 的 PC 相對呼叫'

一個重定位指明在哪(偏移量)、修補什麼(符號 foo)、以及如何修補(型別 R_X86_64_PLT32);一旦 foo 的位址已知,連結器就填上那個洞。

連結期重定位由連結器解析一次;載入期重定位每次啟動都由動態連結器重做。較多載入期重定位意味著較慢的啟動,以及無法在行程間共享的記憶體頁——這是 PIC 把它們降到最少的關鍵原因。

又稱
relocation entry.rela.textfixup位址修正