Linux中對【庫函數】的調用進行跟蹤的 3 種【插樁】技巧


作 者:道哥,10+年嵌入式開發老兵,專注於:C/C++、嵌入式、Linux

關注下方公眾號,回復【書籍】,獲取 Linux、嵌入式領域經典書籍;回復【PDF】,獲取所有原創文章( PDF 格式)。

目錄

別人的經驗,我們的階梯!

什么是插樁?

在稍微具有一點規模的代碼中(C 語言),調用第三方動態庫中的函數來完成一些功能,是很常見的工作場景。

假設現在有一項任務:需要在調用某個動態庫中的某個函數的之前和之后,做一些額外的處理工作。

這樣的需求一般稱作:插樁,也就是對於一個指定的目標函數,我們新建一個包裝函數,來完成一些額外的功能。

在包裝函數中去調用真正的目標函數,但是在調用之前或者之后,可以做一些額外的事情。

比如:統計函數的調用次數、驗證函數的輸入參數是否合法等等

關於程序插樁的官方定義,可以看一下【百度百科】中的描述:

  1. 程序插樁,最早是由J.C. Huang 教授提出的。

  2. 它是在保證被測程序原有邏輯完整性的基礎上在程序中插入一些探針(又稱為“探測儀”,本質上就是進行信息采集的代碼段,可以是賦值語句或采集覆蓋信息的函數調用)。

  3. 通過探針的執行並拋出程序運行的特征數據,通過對這些數據的分析,可以獲得程序的控制流和數據流信息,進而得到邏輯覆蓋等動態信息,從而實現測試目的的方法。

  4. 根據探針插入的時間可以分為目標代碼插樁和源代碼插樁。

這篇文章,我們就一起討論一下:在 Linux 環境下的 C 語言開發中,可以通過哪些方法來實現插樁功能。

插樁示例代碼分析

示例代碼很簡單:

├── app.c
└── lib
    ├── rd3.h
    └── librd3.so

假設動態庫librd3.so是由第三方提供的,里面有一個函數:int rd3_func(int, int);

// lib/rd3.h

#ifndef _RD3_H_
#define _RD3_H_
extern int rd3_func(int, int);
#endif

在應用程序app.c中,調用了動態庫中的這個函數:

app.c代碼如下:

#include <stdio.h>
#include <stdlib.h>
#include "rd3.h"

int main(int argc, char *argv[])
{
    int result = rd3_func(1, 1);
    printf("result = %d \n", result);
    return 0;
}

編譯:

$ gcc -o app app.c -I./lib -L./lib -lrd3 -Wl,--rpath=./lib
  1. -L./lib: 指定編譯時,在 lib 目錄下搜尋庫文件。

  2. -Wl,--rpath=./lib: 指定執行時,在 lib 目錄下搜尋庫文件。

生成可執行程序:app,執行:

$ ./app
result = 3

示例代碼足夠簡單了,稱得上是helloworld的兄弟版本!

在編譯階段插樁

對函數進行插樁,基本要求是:不應該對原來的文件(app.c)進行額外的修改

由於app.c文件中,已經include "rd3.h"了,並且調用了其中的rd3_func(int, int)函數。

所以我們需要新建一個假的 "rd3.h" 提供給app.c,並且要把函數rd3_func(int, int)"重導向"到一個包裝函數,然后在包裝函數中去調用真正的目標函數,如下圖所示:

"重導向"函數:可以使用宏來實現。

包裝函數:新建一個C文件,在這個文件中,需要 #include "lib/rd3.h",然后調用真正的目標文件。

完整的文件結構如下:

├── app.c
├── lib
│   ├── librd3.so
│   └── rd3.h
├── rd3.h
└── rd3_wrap.c

最后兩個文件是新建的:rd3.h, rd3_wrap.c,它們的內容如下:

// rd3.h

#ifndef _LIB_WRAP_H_
#define _LIB_WRAP_H_

// 函數“重導向”,這樣的話 app.c 中才能調用 wrap_rd3_func
#define rd3_func(a, b)   wrap_rd3_func(a, b)

// 函數聲明
extern int wrap_rd3_func(int, int);

#endif
// rd3_wrap.c

#include <stdio.h>
#include <stdlib.h>

// 真正的目標函數
#include "lib/rd3.h"

// 包裝函數,被 app.c 調用
int wrap_rd3_func(int a, int b)
{
    // 在調用目標函數之前,做一些處理
    printf("before call rd3_func. do something... \n");
    
    // 調用目標函數
    int c = rd3_func(a, b);
    
    // 在調用目標函數之后,做一些處理
    printf("after call rd3_func. do something... \n");
    
    return c;
}

app.c 和 rd3_wrap.c一起編譯:

$ gcc -I./ -L./lib -Wl,--rpath=./lib -o app app.c rd3_wrap.c -lrd3

頭文件的搜索路徑不能錯:必須在當前目錄下搜索rd3.h,這樣的話,app.c中的#include "rd3.h" 找到的才是我們新增的那個頭文件 rd3.h

所以在編譯指令中,第一個選項就是 -I./,表示在當前目錄下搜尋頭文件。

另外,由於在rd3_wrap.c文件中,使用#include "lib/rd3.h"來包含庫中的頭文件,因此在編譯指令中,就不需要指定到lib 目錄下去查找頭文件了。

編譯得到可執行程序app,執行一下:

$ ./app 
before call rd3_func. do something... 
after call rd3_func. do something... 
result = 3 

完美!

鏈接階段插樁

Linux 系統中的鏈接器功能是非常強大的,它提供了一個選項:--wrap f,可以在鏈接階段進行插樁

這個選項的作用是:告訴鏈接器,遇到f符號時解析成__wrap_f,在遇到__real_f符號時解析成f,正好是一對!

我們就可以利用這個屬性,新建一個文件rd3_wrap.c,並且定義一個函數__wrap_rd3_func(int, int),在這個函數中去調用__real_rd3_func函數。

只要在編譯選項中加上-Wl,--wrap,rd3_func, 編譯器就會:

  1. 把 app.c 中的 rd3_func 符號,解析成 __wrap_rd3_func,從而調用包裝函數;

  2. 把 rd3_wrap.c 中的 __real_rd3_func 符號,解析成 rd3_func,從而調用真正的函數。

這幾個符號的轉換,是由鏈接器自動完成的!

按照這個思路,一起來測試一下。

文件目錄結構如下:

.
├── app.c
├── lib
│   ├── librd3.so
│   └── rd3.h
├── rd3_wrap.c
└── rd3_wrap.h

rd3_wrap.h是被app.c引用的,內容如下:

#ifndef _RD3_WRAP_H_
#define _RD3_WRAP_H_
extern int __wrap_rd3_func(int, int);
#endif

rd3_wrap.c的內容如下:

#include <stdio.h>
#include <stdlib.h>

#include "rd3_wrap.h"

// 這里不能直接飲用 lib/rd3.h 中的函數了,而要由鏈接器來完成解析。
extern int __real_rd3_func(int, int);

// 包裝函數
int __wrap_rd3_func(int a, int b)
{
    // 在調用目標函數之前,做一些處理
    printf("before call rd3_func. do something... \n");
    
    // 調用目標函數,鏈接器會解析成 rd3_func。
    int c = __real_rd3_func(a, b);
    
    // 在調用目標函數之后,做一些處理
    printf("after call rd3_func. do something... \n");
    
    return c;
}

rd3_wrap.c中,不能直接去 include "rd3.h",因為lib/rd3.h中的函數聲明是int rd3_func(int, int);,沒有__real前綴。

編譯一下:

$ gcc -I./lib -L./lib -Wl,--rpath=./lib -Wl,--wrap,rd3_func -o app app.c rd3_wrap.c -lrd3

注意:這里的頭文件搜索路徑仍然設置為-I./lib,是因為app.cinclude了這個頭文件

得到可執行程序app,執行:

$ ./app
before call rd3_func. do something... 
before call rd3_func. do something... 
result = 3

完美!

執行階段插樁

編譯階段插樁,新建的文件rd3_wrap.c是與app.c一起編譯的,其中的包裝函數名是wrap_rd3_func

app.c中通過一個宏定義實現函數的"重導向"rd3_func --> wrap_rd3_func

我們還可以直接"霸王硬上弓":在新建的文件rd3_wrap.c中,直接定義rd3_func函數。

然后在這個函數中通過dlopen, dlsym系列函數來動態的打開真正的動態庫,查找其中的目標文件,然后調用真正的目標函數。

當然了,這樣的話在編譯app.c時,就不能連接lib/librd3.so文件了。

按照這個思路繼續實踐!

文件目錄結構如下:

├── app.c
├── lib
│   ├── librd3.so
│   └── rd3.h
└── rd3_wrap.c

rd3_wrap.c文件的內容如下(一些錯誤檢查就暫時忽略了):

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

// 庫的頭文件
#include "rd3.h"

// 與目標函數簽名一致的函數類型
typedef int (*pFunc)(int, int);

int rd3_func(int a, int b)
{
    printf("before call rd3_func. do something... \n");
    
    //打開動態鏈接庫
    void *handle = dlopen("./lib/librd3.so", RTLD_NOW);
    
    // 查找庫中的目標函數
    pFunc pf = dlsym(handle, "rd3_func");
    
    // 調用目標函數
    int c = pf(a, b);
    
    // 關閉動態庫句柄
    dlclose(handle);
    
    printf("after call rd3_func. do something... \n");
    return c;
}

編譯包裝的動態庫

$ gcc -shared -fPIC -I./lib -o librd3_wrap.so rd3_wrap.c

得到包裝的動態庫: librd3_wrap.so

編譯可執行程序,需要鏈接包裝庫 librd3_wrap.so

$ gcc -I./lib -L./ -o app app.c -lrd3_wrap -ldl

得到可執行程序app,執行:

$ ./app 
before call rd3_func. do something... 
after call rd3_func. do something... 
result = 3

完美!


------ End ------

文中的測試代碼,已經放在網盤了。

在公眾號【IOT物聯網小鎮】后台回復關鍵字:220109,即可獲取下載地址。

原創不易,請支持一下道哥,把文章分享給更多的嵌入式小伙伴,謝謝!

推薦閱讀

【1】《Linux 從頭學》系列文章

【2】C語言指針-從底層原理到花式技巧,用圖文和代碼幫你講解透徹

【3】原來gdb的底層調試原理這么簡單

【4】內聯匯編很可怕嗎?看完這篇文章,終結它!

其他系列專輯:精選文章應用程序設計物聯網C語言

星標公眾號,第一時間看文章!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM