配置器插入(LD_PRELOAD)
/ el-dee-PEE-relode /
假設一棟大樓有一個總機,每一通打給「malloc」這個字的內線都會被轉接給此刻接那條線的人。平常是標準 C 函式庫接。但若你不必重新拉線,就能溜進去自己接那條線——讓每一通本要打給內建 malloc 的電話改而抵達你的程式碼,會怎樣?在 Linux 上這就是配置器插入(allocator interposition):你讓自己的 malloc/free 先被載入,而由於動態連結解析名稱的方式,整個程式(以及它所用的函式庫)會改而呼叫你的,無須重新編譯。
常見的機制是 LD_PRELOAD 環境變數。當你把 LD_PRELOAD 設成指向一個共享函式庫再執行程式,動態連結器會在所有其他函式庫之前載入那個函式庫。因為符號解析取它找到的第一個定義,你函式庫的 malloc() 與 free() 會在 glibc 的之前被找到,於是行程中任何地方對 malloc() 的每一次呼叫都繫結到你的版本——這就是符號插入(symbol interposition)。你的替代品可以做任何事:它可能是一整個替代配置器(用預載換進 tcmalloc 或 jemalloc 正是這麼做),或是一層薄包裝,記錄每一次配置與釋放以偵測洩漏、把釋放的記憶體填上毒值樣式以抓出釋放後使用、或為剖析計次與計時。標準的指令 $ LD_PRELOAD=./mymalloc.so ./app 就是全部所需。
為何如此有用:它讓你能從外部改變或檢測一個程式的記憶體行為,無須觸碰甚至無須擁有它的原始碼——對除錯、剖析、以及在你無法重建的二進位上換進更快的配置器都很完美。誠實的提醒:一個正確的插入式 malloc 出乎意料地棘手(它必須滿足對齊與 malloc(0) 規則,且不能在自己啟動期間呼叫 malloc(),這是個雞生蛋的陷阱);LD_PRELOAD 是 Linux/glibc 的機制(其他系統有自己的,而靜態連結的二進位根本無法被預載);且基於安全,它對 set-user-id 程式會被忽略。插入是一把利器,不是無害的空操作。
/* mymalloc.c:一層計次後轉發的包裝 */ #define _GNU_SOURCE #include <dlfcn.h> static size_t count; void *malloc(size_t n) { static void *(*real)(size_t); if (!real) real = dlsym(RTLD_NEXT, "malloc"); /* 真正的那個 */ count++; return real(n); } /* $ gcc -shared -fPIC mymalloc.c -ldl -o mymalloc.so */ /* $ LD_PRELOAD=./mymalloc.so ./app <- app 現在呼叫我們的 */
預載一個定義了 malloc 的函式庫,連結器就把整個程式繫結到它——免重編,也不需原始碼。
插入只對動態連結的二進位有效,且對 set-user-id 程式會被忽略;靜態連結的可執行檔已把 malloc 烤進去,無法被預載。