Linux中對【庫函數(shù)】的調用進行跟蹤的三種【插樁】技巧
什么是插樁?
在稍微具有一點規(guī)模的代碼中(C 語言),調用第三方動態(tài)庫中的函數(shù)來完成一些功能,是很常見的工作場景。
假設現(xiàn)在有一項任務:需要在調用某個動態(tài)庫中的某個函數(shù)的之前和之后,做一些額外的處理工作。
這樣的需求一般稱作:插樁,也就是對于一個指定的目標函數(shù),新建一個包裝函數(shù),來完成一些額外的功能。
在包裝函數(shù)中去調用真正的目標函數(shù),但是在調用之前或者之后,可以做一些額外的事情。
比如:統(tǒng)計函數(shù)的調用次數(shù)、驗證函數(shù)的輸入?yún)?shù)是否合法等等。
關于程序插樁的官方定義,可以看一下【百度百科】中的描述:
- 程序插樁,最早是由J.C. Huang 教授提出的。
- 它是在保證被測程序原有邏輯完整性的基礎上在程序中插入一些探針(又稱為“探測儀”,本質上就是進行信息采集的代碼段,可以是賦值語句或采集覆蓋信息的函數(shù)調用)。
- 通過探針的執(zhí)行并拋出程序運行的特征數(shù)據(jù),通過對這些數(shù)據(jù)的分析,可以獲得程序的控制流和數(shù)據(jù)流信息,進而得到邏輯覆蓋等動態(tài)信息,從而實現(xiàn)測試目的的方法。
- 根據(jù)探針插入的時間可以分為目標代碼插樁和源代碼插樁。
這篇文章,我們就一起討論一下:在 Linux 環(huán)境下的 C 語言開發(fā)中,可以通過哪些方法來實現(xiàn)插樁功能。
插樁示例代碼分析
示例代碼很簡單:
- ├── app.c
- └── lib
- ├── rd3.h
- └── librd3.so
假設動態(tài)庫librd3.so是由第三方提供的,里面有一個函數(shù):int rd3_func(int, int);。
- // lib/rd3.h
- #ifndef _RD3_H_
- #define _RD3_H_
- extern int rd3_func(int, int);
- #endif
在應用程序app.c中,調用了動態(tài)庫中的這個函數(shù):
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
- -L./lib: 指定編譯時,在 lib 目錄下搜尋庫文件。
- -Wl,--rpath=./lib: 指定執(zhí)行時,在 lib 目錄下搜尋庫文件。
生成可執(zhí)行程序:app,執(zhí)行:
- $ ./app
- result = 3
示例代碼足夠簡單了,稱得上是helloworld的兄弟版本!
在編譯階段插樁
對函數(shù)進行插樁,基本要求是:不應該對原來的文件(app.c)進行額外的修改。
由于app.c文件中,已經(jīng)include "rd3.h"了,并且調用了其中的rd3_func(int, int)函數(shù)。
所以我們需要新建一個假的 "rd3.h" 提供給app.c,并且要把函數(shù)rd3_func(int, int)"重導向"到一個包裝函數(shù),然后在包裝函數(shù)中去調用真正的目標函數(shù),如下圖所示:
"重導向"函數(shù):可以使用宏來實現(xiàn)。
包裝函數(shù):新建一個C文件,在這個文件中,需要 #include "lib/rd3.h",然后調用真正的目標文件。
完整的文件結構如下:
- ├── app.c
- ├── lib
- │ ├── librd3.so
- │ └── rd3.h
- ├── rd3.h
- └── rd3_wrap.c
最后兩個文件是新建的:rd3.h, rd3_wrap.c,它們的內(nèi)容如下:
- // rd3.h
- #ifndef _LIB_WRAP_H_
- #define _LIB_WRAP_H_
- // 函數(shù)“重導向”,這樣的話 app.c 中才能調用 wrap_rd3_func
- #define rd3_func(a, b) wrap_rd3_func(a, b)
- // 函數(shù)聲明
- extern int wrap_rd3_func(int, int);
- #endif
- // rd3_wrap.c
- #include <stdio.h>
- #include <stdlib.h>
- // 真正的目標函數(shù)
- #include "lib/rd3.h"
- // 包裝函數(shù),被 app.c 調用
- int wrap_rd3_func(int a, int b)
- {
- // 在調用目標函數(shù)之前,做一些處理
- printf("before call rd3_func. do something... \n");
- // 調用目標函數(shù)
- int c = rd3_func(a, b);
- // 在調用目標函數(shù)之后,做一些處理
- 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 目錄下去查找頭文件了。
編譯得到可執(zhí)行程序app,執(zhí)行一下:
- $ ./app
- before call rd3_func. do something...
- after call rd3_func. do something...
- result = 3
完美!
鏈接階段插樁
Linux 系統(tǒng)中的鏈接器功能是非常強大的,它提供了一個選項:--wrap f,可以在鏈接階段進行插樁。
這個選項的作用是:告訴鏈接器,遇到f符號時解析成__wrap_f,在遇到__real_f符號時解析成f,正好是一對!
我們就可以利用這個屬性,新建一個文件rd3_wrap.c,并且定義一個函數(shù)__wrap_rd3_func(int, int),在這個函數(shù)中去調用__real_rd3_func函數(shù)。
只要在編譯選項中加上-Wl,--wrap,rd3_func, 編譯器就會:
- 把 app.c 中的 rd3_func 符號,解析成 __wrap_rd3_func,從而調用包裝函數(shù);
- 把 rd3_wrap.c 中的 __real_rd3_func 符號,解析成 rd3_func,從而調用真正的函數(shù)。
這幾個符號的轉換,是由鏈接器自動完成的!
按照這個思路,一起來測試一下。
文件目錄結構如下:
- .
- ├── app.c
- ├── lib
- │ ├── librd3.so
- │ └── rd3.h
- ├── rd3_wrap.c
- └── rd3_wrap.h
rd3_wrap.h是被app.c引用的,內(nèi)容如下:
- #ifndef _RD3_WRAP_H_
- #define _RD3_WRAP_H_
- extern int __wrap_rd3_func(int, int);
- #endif
rd3_wrap.c的內(nèi)容如下:
- #include <stdio.h>
- #include <stdlib.h>
- #include "rd3_wrap.h"
- // 這里不能直接飲用 lib/rd3.h 中的函數(shù)了,而要由鏈接器來完成解析。
- extern int __real_rd3_func(int, int);
- // 包裝函數(shù)
- int __wrap_rd3_func(int a, int b)
- {
- // 在調用目標函數(shù)之前,做一些處理
- printf("before call rd3_func. do something... \n");
- // 調用目標函數(shù),鏈接器會解析成 rd3_func。
- int c = __real_rd3_func(a, b);
- // 在調用目標函數(shù)之后,做一些處理
- printf("after call rd3_func. do something... \n");
- return c;
- }
rd3_wrap.c中,不能直接去 include "rd3.h",因為lib/rd3.h中的函數(shù)聲明是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.c中include了這個頭文件。
得到可執(zhí)行程序app,執(zhí)行:
- $ ./app
- before call rd3_func. do something...
- before call rd3_func. do something...
- result = 3
完美!
執(zhí)行階段插樁
在編譯階段插樁,新建的文件rd3_wrap.c是與app.c一起編譯的,其中的包裝函數(shù)名是wrap_rd3_func。
app.c中通過一個宏定義實現(xiàn)函數(shù)的"重導向":rd3_func --> wrap_rd3_func。
我們還可以直接"霸王硬上弓":在新建的文件rd3_wrap.c中,直接定義rd3_func函數(shù)。
然后在這個函數(shù)中通過dlopen, dlsym系列函數(shù)來動態(tài)的打開真正的動態(tài)庫,查找其中的目標文件,然后調用真正的目標函數(shù)。
當然了,這樣的話在編譯app.c時,就不能連接lib/librd3.so文件了。
按照這個思路繼續(xù)實踐!
文件目錄結構如下:
- ├── app.c
- ├── lib
- │ ├── librd3.so
- │ └── rd3.h
- └── rd3_wrap.c
rd3_wrap.c文件的內(nèi)容如下(一些錯誤檢查就暫時忽略了):
- #include <stdio.h>
- #include <stdlib.h>
- #include <dlfcn.h>
- // 庫的頭文件
- #include "rd3.h"
- // 與目標函數(shù)簽名一致的函數(shù)類型
- typedef int (*pFunc)(int, int);
- int rd3_func(int a, int b)
- {
- printf("before call rd3_func. do something... \n");
- //打開動態(tài)鏈接庫
- void *handle = dlopen("./lib/librd3.so", RTLD_NOW);
- // 查找?guī)熘械哪繕撕瘮?shù)
- pFunc pf = dlsym(handle, "rd3_func");
- // 調用目標函數(shù)
- int c = pf(a, b);
- // 關閉動態(tài)庫句柄
- dlclose(handle);
- printf("after call rd3_func. do something... \n");
- return c;
- }
編譯包裝的動態(tài)庫:
- $ gcc -shared -fPIC -I./lib -o librd3_wrap.so rd3_wrap.c
得到包裝的動態(tài)庫: librd3_wrap.so。
編譯可執(zhí)行程序,需要鏈接包裝庫 librd3_wrap.so:
- $ gcc -I./lib -L./ -o app app.c -lrd3_wrap -ldl
得到可執(zhí)行程序app,執(zhí)行:
- $ ./app
- before call rd3_func. do something...
- after call rd3_func. do something...
- result = 3
完美!
本文轉載自微信公眾號「IOT物聯(lián)網(wǎng)小鎮(zhèn)」