函式庫插入與目的檔工具 (Library Interpositioning & Tools)

Overview Table

主題 核心內容 書頁
Library Interpositioning 攔截對 shared library 函式的呼叫,改為執行自己的 wrapper function p.743-744
Compile-Time 插入 C preprocessor#definemalloc 換成 mymalloc;需 gcc -I. p.744-745
Link-Time 插入 靜態連結器 --wrap f 旗標;f__wrap_f__real_ff p.744-746
Run-Time 插入 LD_PRELOAD 環境變數 + dlsym(RTLD_NEXT, ...);只需可執行檔 p.746-748
目的檔工具 GNU binutils:arstringsstripnmsizereadelfobjdump;另有 ldd p.749
章節總結 連結兩大任務:symbol resolutionrelocation;三種目的檔形式 p.749-750

7.13 函式庫插入 (Library Interpositioning) (p.743)

Linux 連結器支援一種強大技術 library interpositioning:攔截對 shared library 函式的呼叫、改為執行你自己的程式碼。應用場景:

基本想法:針對要插入的 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 timelink timerun time(程式載入執行時)。書中以 int.c 為執行範例:呼叫 malloc(32) 從 heap 配置 32 bytes,再 free(p) 歸還,目標是追蹤這兩個呼叫 (p.744-745)。

三種機制的存取需求遞減:compile-time 需要原始碼 → link-time 需要 relocatable object files → run-time 只需要可執行檔 (executable object file)。這是考試最常考的區辨點 (p.746)。

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

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
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 環境變數上:

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
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
run-time wrapper 與 target 同名(都叫 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_ff)
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) sizenm
objdump 所有 binary 工具之母」;可顯示 object file 全部資訊,最有用的功能是反組譯 .text section 的二進位指令
ldd (非 binutils)列出可執行檔執行期需要的 shared libraries
記法:readelfsize + nm;要看反組譯用 objdump -d;要看動態相依用 ldd。這些工具在分析 07-Linking/01-Static-Linking-and-Object-Files 的 ELF 結構時會反覆用到。

7.15 章節總結 (p.749-750)

書末補充 (Bibliographic Notes, p.750):連結位於編譯器、計算機結構、作業系統的交界,經典教科書少有完整涵蓋;Levine 的專書是主要參考;ELF 與 DWARF(.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_ff
「有原始碼,想在編譯期換掉函式」 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 resolutionrelocation
「object file 的三種形式?」 relocatableexecutableshared