函式庫插入與目的檔工具 (Library Interpositioning & Tools)
Overview Table
| 主題 | 核心內容 | 書頁 |
|---|---|---|
| Library Interpositioning | 攔截對 shared library 函式的呼叫,改為執行自己的 wrapper function | p.743-744 |
| Compile-Time 插入 | 用 C preprocessor 的 #define 把 malloc 換成 mymalloc;需 gcc -I. |
p.744-745 |
| Link-Time 插入 | 靜態連結器 --wrap f 旗標;f → __wrap_f、__real_f → f |
p.744-746 |
| Run-Time 插入 | LD_PRELOAD 環境變數 + dlsym(RTLD_NEXT, ...);只需可執行檔 |
p.746-748 |
| 目的檔工具 | GNU binutils:ar、strings、strip、nm、size、readelf、objdump;另有 ldd |
p.749 |
| 章節總結 | 連結兩大任務:symbol resolution 與 relocation;三種目的檔形式 | p.749-750 |
7.13 函式庫插入 (Library Interpositioning) (p.743)
Linux 連結器支援一種強大技術 library interpositioning:攔截對 shared library 函式的呼叫、改為執行你自己的程式碼。應用場景:
- 追蹤某函式庫函式被呼叫的次數
- 驗證/追蹤函式的輸入輸出值
- 甚至完全替換成不同的實作(例如換掉
malloc的配置策略)
基本想法:針對要插入的 target function,建立一個 prototype 完全相同的 wrapper function,再用某種插入機制「騙」系統去呼叫 wrapper 而非 target。wrapper 通常執行自己的邏輯 → 呼叫 target → 把回傳值傳回呼叫者。
呼叫者 (caller)
│ call malloc(32) ← 呼叫者以為在呼叫 target
▼
┌─────────────────────────┐
│ Wrapper function │ 1. 執行自己的邏輯(如印 trace)
│ (prototype 與 target │ 2. 呼叫真正的 target function
│ 完全相同) │ 3. 回傳 target 的回傳值
└───────────┬─────────────┘
│ call (real) malloc
▼
┌─────────────────────────┐
│ Target function │ libc.so 中真正的 malloc
└─────────────────────────┘
插入可發生在三個時間點:compile time、link time、run time(程式載入執行時)。書中以 int.c 為執行範例:呼叫 malloc(32) 從 heap 配置 32 bytes,再 free(p) 歸還,目標是追蹤這兩個呼叫 (p.744-745)。
7.13.1 Compile-Time 插入 (p.744-745)
利用 C preprocessor 在編譯時替換呼叫。本地 malloc.h 用巨集把 target 換成 wrapper:
/* 本地 malloc.h */
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)
void *mymalloc(size_t size);
void myfree(void *ptr);
wrapper(mymalloc.c,#ifdef COMPILETIME 區塊)呼叫真正的 malloc、印出 trace、回傳:
linux> gcc -DCOMPILETIME -c mymalloc.c
linux> gcc -I. -o intc int.c mymalloc.o
- 插入的關鍵是
-I.:告訴 preprocessor 先在目前目錄尋找malloc.h,才找系統目錄 →int.c引到的是本地(巨集替換版)的malloc.h - wrapper 本身 (
mymalloc.c) 則用標準malloc.h編譯,所以它內部的malloc呼叫不會被替換 - 執行
./intc得到 trace:malloc(32)=0x9ee010/free(0x9ee010)
7.13.2 Link-Time 插入 (p.744-746)
Linux 靜態連結器支援 --wrap f 旗標,改寫符號解析規則:
| 原始參考 (reference) | 解析為 (resolved to) |
|---|---|
f |
__wrap_f(前綴為兩個底線) |
__real_f |
f(真正的 target) |
wrapper(#ifdef LINKTIME 區塊)長這樣:
void *__wrap_malloc(size_t size)
{
void *ptr = __real_malloc(size); /* 呼叫 libc malloc */
printf("malloc(%d) = %p\n", (int)size, ptr);
return ptr;
}
編譯與連結:
linux> gcc -DLINKTIME -c mymalloc.c
linux> gcc -c int.c
linux> gcc -Wl,--wrap,malloc -Wl,--wrap,free -o intl int.o mymalloc.o
-Wl,option把 option 傳給連結器,option 中每個逗號被替換為空格:-Wl,--wrap,malloc→ 傳給 linker 的是--wrap malloc- 符號解析流程:
int.o的malloc參考 →__wrap_malloc;__wrap_malloc內的__real_malloc參考 → 真正的malloc
int.o: call malloc ──--wrap 改寫──▶ __wrap_malloc (mymalloc.o)
│
│ call __real_malloc
▼
──--wrap 改寫──▶ malloc (libc)
7.13.3 Run-Time 插入 (p.746-748)
只需可執行檔即可插入,機制建立在動態連結器的 LD_PRELOAD 環境變數上:
LD_PRELOAD設為一串 shared library 路徑(以空格或冒號分隔)時,載入執行程式時,dynamic linker (ld-linux.so) 會先搜尋 LD_PRELOAD 中的函式庫、才搜尋其他 shared libraries 來解析未定義參考- 因此可以對任何可執行檔中任何 shared library(包括
libc.so)的任何函式插入
wrapper(#ifdef RUNTIME 區塊)直接命名為 malloc,並用 dlsym(RTLD_NEXT, "malloc") 取得下一個(即 libc 真正的)malloc 位址,避免無限遞迴:
#define _GNU_SOURCE
#include <dlfcn.h>
void *malloc(size_t size)
{
void *(*mallocp)(size_t size);
char *error;
mallocp = dlsym(RTLD_NEXT, "malloc"); /* 取得 libc malloc 位址 */
if ((error = dlerror()) != NULL) {
fputs(error, stderr);
exit(1);
}
char *ptr = mallocp(size); /* 呼叫 libc malloc */
printf("malloc(%d) = %p\n", (int)size, ptr);
return ptr;
}
建置與執行:
linux> gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl
linux> gcc -o intr int.c
linux> LD_PRELOAD="./mymalloc.so" ./intr # bash
linux> (setenv LD_PRELOAD "./mymalloc.so"; ./intr; unsetenv LD_PRELOAD) # csh/tcsh
- wrapper library 必須以
-shared -fpic建成 PIC 的 shared library,並以-ldl連結 dlopen 介面(參見 07-Linking/03-Executable-Loading-and-Dynamic-Linking) - 書中示範
LD_PRELOAD="./mymalloc.so" /usr/bin/uptime:可對現成的系統程式插入,追蹤到uptime內部十多次malloc/free呼叫 (p.747)
LD_PRELOAD="./mymalloc.so" ./intr
載入時 dynamic linker 解析 intr 的未定義參考:
搜尋順序: mymalloc.so ──▶ libc.so ──▶ 其他 shared libs
(LD_PRELOAD (之後才輪到)
優先)
⇒ intr 的 malloc 綁定到 mymalloc.so 的 wrapper
wrapper 內以 dlsym(RTLD_NEXT,"malloc") 找「搜尋順序中的下一個」
⇒ 綁定到 libc.so 的真 malloc
malloc),若在 wrapper 內直接寫 malloc(size) 會呼叫到自己造成無限遞迴;必須透過 dlsym(RTLD_NEXT, ...) 取得真正的 libc 版本。另外書中 free wrapper 對 NULL 指標直接 return,符合 libc 語意。三種插入機制比較表
| 機制 | 時間點 | 需要的存取權 | 關鍵手段 | 關鍵指令/旗標 |
|---|---|---|---|---|
| Compile-time | 前處理/編譯時 | 原始碼 (.c) |
preprocessor 巨集替換 | #define malloc(size) mymalloc(size)、gcc -I. |
| Link-time | 靜態連結時 | relocatable object files (.o) |
連結器符號改名 | -Wl,--wrap,malloc(f→__wrap_f,__real_f→f) |
| Run-time | 載入/執行時 | 只需可執行檔 | dynamic linker 搜尋順序 | LD_PRELOAD + dlsym(RTLD_NEXT, "f") |
7.14 操作目的檔的工具 (p.749)
GNU binutils 套件在所有 Linux 平台皆可用,協助理解與操作 object files:
| 工具 | 功能 |
|---|---|
ar |
建立 static libraries;插入、刪除、列出、抽取成員 |
strings |
列出 object file 中所有可印出字串 |
strip |
刪除 object file 的 symbol table 資訊 |
nm |
列出 object file symbol table 中定義的符號 |
size |
列出各 section 的名稱與大小 |
readelf |
顯示 object file 完整結構(含 ELF header 全部資訊);功能涵蓋 (subsumes) size 與 nm |
objdump |
「所有 binary 工具之母」;可顯示 object file 全部資訊,最有用的功能是反組譯 .text section 的二進位指令 |
ldd |
(非 binutils)列出可執行檔執行期需要的 shared libraries |
readelf ⊇ size + nm;要看反組譯用 objdump -d;要看動態相依用 ldd。這些工具在分析 07-Linking/01-Static-Linking-and-Object-Files 的 ELF 結構時會反覆用到。7.15 章節總結 (p.749-750)
- 連結可在 compile time 由 static linker 執行,或在 load time / run time 由 dynamic linker 執行
- 連結器操作的二進位檔稱 object files,共三種形式:relocatable、executable、shared
- 連結器兩大任務:
- Symbol resolution:把 object file 中每個 global symbol 綁定到唯一定義
- Relocation:決定每個 symbol 的最終記憶體位址,並修改對這些物件的參考
- static linker 由 compiler driver(如
gcc)呼叫,把多個 relocatable object files 合併成一個 executable object file;多個檔案定義同名符號時,連結器默默解析多重定義的規則可能引入微妙的 bug(參見 07-Linking/02-Symbol-Resolution-and-Relocation) - 多個 object files 可打包成 static library;許多連結器採由左至右循序掃描解析符號參考,是另一個造成困惑的連結期錯誤來源
- Loader 把 executable file 內容映射到記憶體並執行;對部分連結的執行檔,loader 在載入時呼叫 dynamic linker,由它載入 shared library 並 relocate 程式中的參考,完成連結
- 以 position-independent code (PIC) 編譯的 shared libraries 可載入到任意位置,並在執行期由多個 processes 共享;應用程式也可在執行期透過 dynamic linker(
dlopen介面)載入、連結並存取 shared libraries 的函式與資料
.debug、.line sections 的規格)及 x86-64 ABI 則定義 relocation 與 PIC 的正式規則。Exam/Test Patterns
| 情境 / 關鍵字 | 答案 |
|---|---|
| 「只有可執行檔、沒有原始碼與 .o,要追蹤 malloc」 | Run-time 插入:LD_PRELOAD + dlsym(RTLD_NEXT, "malloc") |
| 「有 .o 檔但沒有原始碼,要包裝 f」 | Link-time 插入:gcc -Wl,--wrap,f;f→__wrap_f、__real_f→f |
| 「有原始碼,想在編譯期換掉函式」 | Compile-time 插入:本地 header 用 #define 替換 + gcc -I. |
-Wl,--wrap,malloc 傳給 linker 的實際內容 |
--wrap malloc(逗號換成空格) |
run-time wrapper 內直接呼叫 malloc 會怎樣 |
呼叫到 wrapper 自己 → 無限遞迴;須用 dlsym(RTLD_NEXT, ...) |
LD_PRELOAD 的效果 |
dynamic linker 解析未定義參考時優先搜尋列出的 shared libraries |
| 建 run-time wrapper library 的編譯旗標 | gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl |
| 「哪個工具反組譯 .text?」 | objdump(binary 工具之母) |
| 「哪個工具涵蓋 size 與 nm 的功能?」 | readelf |
| 「列出執行檔需要哪些 shared libraries」 | ldd |
| 「刪除 symbol table」/「列出可印字串」/「建 static library」 | strip / strings / ar |
| 「連結器的兩大任務?」 | symbol resolution 與 relocation |
| 「object file 的三種形式?」 | relocatable、executable、shared |