符號插入取代(symbol interposition)
假設一個程式與數個共享函式庫最終都呼叫 malloc()。現在你想塞進自己的版本——也許是為了計數配置或偵測洩漏——而不重新編譯任何東西。符號插入取代是讓某個符號的一個定義在載入時頂替另一個的機制,使得大家對 malloc() 的呼叫都改走你的版本而非 C 函式庫的。這就是洩漏偵測器與記憶體分析器之類的工具從外部掛進程式的方法。
在 ELF 動態連結中,查找一個名字時,動態連結器以一個既定順序搜尋已載入的物件,而「第一個」相符的定義對所有人勝出——那第一個定義插入取代(搶占)所有後面同名的。你可以用環境變數 LD_PRELOAD 把你的函式庫強推到搜尋最前面,它在其餘之前載入你的共享物件,於是你那個強的、預設可見性、名為 malloc 的符號最先被找到、到處被用。這只對預設可見性的符號有效;HIDDEN 符號不在動態表裡、無法被插入取代,而 PROTECTED 符號對於從它自己物件內部發出的呼叫無法被插入取代。
它既是強大的工具,也是隱藏的成本。工具面:LD_PRELOAD 插入取代驅動了 AddressSanitizer 式的包裝、測試用的假時鐘,以及像 tcmalloc 或 jemalloc 這樣可直接替換的配置器。成本面:因為「任何」預設可見性的符號原則上都可能被插入取代,編譯器往往不能假設對你自己函式的呼叫會留在你的函式庫內,所以它必須透過 GOT/PLT 來轉呼叫而非直接呼叫,這花掉一點速度——這是把內部符號標為 hidden 或建構成與位置無關之可執行檔能讓程式碼更快的原因之一。一個常見的意外是:插入取代是依符號名字且對順序敏感的,所以兩個定義同名的預載函式庫會以微妙、依順序的方式互動。
// my_malloc.c -> 建構成 libcount.so,然後: void *malloc(size_t n) { count++; return real_malloc(n); // 透過 dlsym(RTLD_NEXT,...) 查找 } $ LD_PRELOAD=./libcount.so ./a.out // 現在每次 malloc() 都被計數
預載一個共享物件會把它的符號放在搜尋順序最前面,所以它的 malloc() 對整個程式插入取代 C 函式庫的版本。
插入取代只觸及預設可見性的動態符號;HIDDEN 符號完全無法被插入取代。插入取代的可能性也是為什麼預設可見性的呼叫可能走 PLT、比直接呼叫稍慢的原因。