[時間:2017-08] [狀態:Open]
 [關鍵詞:linux, libray,打樁,interposition,函數替換,鏈接器,gcc,malloc,free]
0 引言
本文主要參考《深入理解計算機系統》(原書第三版)ch7.13。作為個人知識整理和后續參考使用。
庫打樁(interposition)這個名詞比較陌生,這是由Linux鏈接器所提供的技術,允許用戶截獲對共享庫函數的調用,並執行自己的代碼(當然是在普通權限下,管理員權限通常是禁止使用該技術的)。
 使用打樁機制,可以追蹤某個特殊庫函數的調用次數、驗證並追蹤其輸入輸出,甚至把它替換成一個完全不同的實現。
2 基本原理
打樁機制的基本實現原理如下:
 給定需要打樁的目標函數,常見一個wrapper函數,其原型和目標函數一致。
 利用特殊的打樁機制,可以實現讓系統調用你的wrapper函數而不是目標函數。
 wrapper函數中通常會執行自己的邏輯,然后調用目標函數,再將目標函數的返回值傳遞給調用者。
打樁可以發生在編譯時、鏈接時或者程序被加載執行的運行時。不同的階段都有對應的打樁機制,也有其局限性。
 下文將以c標准庫中的malloc和free函數的打樁來說明不同打樁機制。基本目標是用打樁來追蹤程序運行時對malloc和free的調用。
3 示例一:編譯時打樁
編譯時打樁說白就是將對目標函數的調用替換為對應wrapper的調用。實現方式很簡單,通過指定編譯指令來實現。
 下面代碼實現了一個demo,用於說明如何使用預處理器實現編譯時打樁。包裝函數實現如下:
// malloc.h
#ifndef COMPILE_TIME
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)
#endif
void * mymalloc(size_t size);
void myfree(void *ptr);
 
         
         
        // mymalloc.cpp
#ifdef COMPILE_TIME
#include <stdio.h>
#include <malloc.h>
// malloc wrapper function
void * mymalloc(size_t size) {
    void * ptr = ::malloc(size);
    printf("malloc %p size %u\n", ptr, size);
    return ptr;
}
// free wrapper function
void myfree(void *ptr) {
    ::free(ptr);
    printf("free %p\n", ptr);
}
#endif
 
        在wrapper函數中我們調用目標函數,並打印追蹤記錄。本地的malloc.h頭文件用於替換系統的目標函數調用邏輯,將其切換到對應的包裝函數中。
 所有源碼可以在我的SampleCode[https://git.oschina.net/Tocy/SampleCode.git]的interposition-打樁\compile目錄找到,可以使用下面函數指令編譯代碼:
gcc -DCOMPILE_TIME -c mymalloc.cpp
gcc -I. -o intpos main.cpp mymalloc.o
 
        編譯后執行可執行文件,就可以得到將對malloc/free的調用轉到我們的wrapper函數中。
4 示例二:鏈接時打樁
Linux靜態鏈接器支持用--wrap f標志進行鏈接時打樁。這個標志告訴鏈接器,把對符號f的引用解析成__wrap_f(前綴是兩個下划線),還要對符號__real_f的引用解析成f。
 我們的wrap函數實現如下:
#ifdef LINK_TIME
#include <stdio.h>
extern "C" {
void * __real_malloc(size_t size);
void __real_free(void * ptr);
// malloc wrapper function
void * __wrap_malloc(size_t size) {
    printf("%s enter %u\n", __FUNCTION__, size);
    void * ptr = __real_malloc(size);
    printf("malloc %p size %u\n", ptr, size);
    return ptr;
}
// free wrapper function
void __wrap_free(void *ptr) {
    __real_free(ptr);
    printf("free %p\n", ptr);
}
}
#endif
 
        所有源碼可以在我的SampleCode[https://git.oschina.net/Tocy/SampleCode.git]的interposition-打樁\link目錄找到使用下面命令編譯:
gcc -DLINK_TIME -c mymalloc.cpp
gcc -c main.cpp
gcc -Wl,--wrap,malloc -Wl,--wrap,free -o intpos main.o mymalloc.o
 
        注意這里是WL(L小寫,不是數字1,Linker option)。-Wl,option標志把option傳遞給鏈接器。option中的每個逗號都會替換為一個空格。即-Wl,--wrap,malloc就是把--wrap malloc傳遞給鏈接器。上面編譯必須分開,否則可能會出錯。
通過鏈接器的命令打樁也可以實現我們的目的,但是有一個缺點,你需要重新連接所有需要監測的模塊。
5 示例三:運行時打樁
編譯時打樁需要訪問程序的源代碼,連接時打樁需要能夠訪問程序的可重定位的對象文件。不過運行時打樁僅需要訪問可執行目標文件即可,它的基本原理是基於動態鏈接器的LD_PRELOAD環境變量的。
 如果LD_PRELOAD環境變量被設置為一個共享庫路徑的列表(以空格或分號分隔),那么當你加載和執行一個程序,需要解析未定義的引用時,動態鏈接器會先搜做LD_PRELOAD中給定的庫,然后才搜索任何其他的庫。有了這個機制,當你加載和執行任意可執行文件時,可以對任何共享庫中任意函數打樁,包括libc.so中的malloc和free。
我們的wrapper函數實現如下
#ifdef RUNTIME
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
// malloc wrapper function
void * malloc(size_t size) {
    printf("%s enter %u\n", __FUNCTION__, size);
    void *(* mallocp)(size_t size);
    char * error;
    
    // get address of libc malloc
    mallocp = dlsym(RTLD_NEXT, "malloc");
    if ((error = dlerror()) != NULL) {
        fputs(error, stderr);
        exit(1);
    }
    void * ptr = mallocp(size);
    printf("malloc %p size %u\n", ptr, (int)size);
    return ptr;
}
// free wrapper function
void free(void *ptr) {
    void (* freep)(void *ptr);
    char * error;
    
    // get address of libc free
    freep = dlsym(RTLD_NEXT, "free");
    if ((error = dlerror()) != NULL) {
        fputs(error, stderr);
        exit(1);
    }
    freep(ptr);
    printf("free %p\n", ptr);
}
#endif
 
        所有源碼可以在我的SampleCode[https://git.oschina.net/Tocy/SampleCode.git]的interposition-打樁\runtime目錄找到,需要通過下面指令編譯:
 gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.cpp -ldl -fpermissive
主函數不做修改。但運行時需要使用下面指令:
 LD_PRELOAD="./mymalloc.so" ./intpos
 如此就可以達到預期的效果,監測對malloc和free函數的調用。
6 補充知識:malloc調試變量——__malloc_hook
 
        如果單純為了處理malloc/free的調用,可以參考下萬能的manual。
 __malloc_hook是glibc提供的malloc調試變量中的一個,詳情參考MALLOC_HOOK。
 只要在代碼中添加__malloc_hook= my_malloc_hook;語句,當前程序中關於的malloc調用都會使用my_malloc_hook函數,簡單方便。但是這組調試變量不是線程安全的,很多新的編譯器已經將該功能廢棄。有興趣的可以參考下。
7 總結
到此,本文開頭的問題已經解決。整理此文目的只是為了加深記憶。
 同時了解下基本的內存泄露分析方法。
