Linux中對【庫函式】的呼叫進行跟蹤的 3 種【插樁】技巧

語言: CN / TW / HK

作  者:道哥,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 ,即可獲取下載地址。

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