效能優(yōu)化實踐 | C/C++單元測試萬能插樁工具

來源 | 騰訊技術(shù)工程
研發(fā)效能是一個涉及面很廣的話題,它涵蓋了軟件交付的整個生命周期,涉及產(chǎn)品、架構(gòu)、開發(fā)、測試、運維,每個環(huán)節(jié)都可能影響順暢、高質(zhì)量地持續(xù)有效交付。在騰訊安全平臺部實際研發(fā)與測試工作中我們發(fā)現(xiàn),代碼插樁隔離是單元測試工作中的一個強需求,然而業(yè)界現(xiàn)有 C/C++插樁工具由于使用上的局限性,運行效率和體驗仍有很大改善空間。本文介紹了團隊基于研效優(yōu)化實踐而自研的動態(tài)插樁工具,旨在實現(xiàn)單元測試的輕量化運行,提高代碼覆蓋率,從而助力研發(fā)團隊的效能提升。
問題&思路
目前存在的 C/C++插樁工具,基本上都有各種使用上的局限,比如流行的 gmock,只能對 C++的虛函數(shù)進行插樁替換,針對非虛函數(shù),則需要先對被測代碼進行改造;同時對于系統(tǒng)接口,C 風(fēng)格的第三方庫代碼,也無能為力。
如果可以繞開編譯器,直接從底層入手,比如做機器指令修改,則可以不受語法及編譯器的束縛,直接達到目的,這樣在使用中就 幾乎不受限制。
原理
C/C++語言編譯后的可執(zhí)行體,其實就是一個個的函數(shù)實現(xiàn),每個函數(shù)的開頭就是它的入口。一個函數(shù) A 調(diào)用另一個函數(shù) B,就是代碼在執(zhí)行過程中,控制流從函數(shù) A 的某處跳到了函數(shù) B 的開頭,所以如果想用一個新的函數(shù) C 取代函數(shù) B,可以在函數(shù) B 的開頭用機器碼的形式寫入如下等價邏輯:
MOVQ ADDRESS_OF_C %RAX //將函數(shù)C的地址放到寄存器RAX
JMPQ *RAX //無條件跳轉(zhuǎn)到RAX所指向的位置
這樣,當(dāng)控制流從函數(shù) A 進入函數(shù) B 的開始位置的時候,即會執(zhí)行上述代碼,從而直接跳轉(zhuǎn)到 C 的開頭處。其最終效果,是所有對函數(shù) B 的調(diào)用,都如同直接調(diào)用了函數(shù) C。
基于上述原理,被插樁的代碼包括第三方庫,如 MySql、其他同事未完成的模塊、甚至是操作系統(tǒng)的 API 接口,如 read、select 等;
同時,樁函數(shù)不僅可以模擬原函數(shù)的返回值,實際上它作為一個普通的 C 函數(shù),對原函數(shù)有完全的操作能力,比如可以訪問傳遞給原函數(shù)調(diào)用真實的參數(shù)、C++成員變量(針對對成員函數(shù)的模擬),給定任意的返回值,訪問全局變量、對調(diào)用進行計數(shù)等。
實際實現(xiàn)中,考慮到不同測試用例間的互不干擾,除了能執(zhí)行函數(shù)替換,還需要在執(zhí)行完一個測試時還原現(xiàn)場。這些具體細節(jié)可以直接參考代碼。
使用
對全局函數(shù)插樁
原始函數(shù):
int global(int a, int b) {
return a + b;
}
對應(yīng)的樁函數(shù):
int fake_global(int a, int b) {
//校驗參數(shù)正確性,確定被測代碼傳入了正確的值
assert(a == 3);
assert(b == 2);
//給一個返回值,配合被測代碼走特定分支
return a - b;
}
插樁示例:
assert(global(3, 2) == 5);
//通過mock調(diào)用,完成函數(shù)動態(tài)替換
assert(0 == mock(&global, &fake_global));
//調(diào)用mock后的函數(shù),可以看到返回值變了
assert(global(3, 2) == 1);
//結(jié)束mock
reset();
//函數(shù)行為恢復(fù)
assert(global(3, 2) == 5);
對普通成員函數(shù)插樁
被測代碼:
class A {
public:
int member(int a) {return ++a;}
static int static_member(int a) {return 200;}
virtual int virtual_member() {return 400;}
};
樁函數(shù):
int fake_member(A *pTihs, int a) {
//由于是對成員函數(shù)插樁,這里需要這個this指針參數(shù)
return --a;
}
插樁示例:
A a;
assert(a.member(100) == 101);
mock(&A::member, fake_member);
assert(a.member(100) == 99);
reset();
assert(a.member(100) == 101);
對靜態(tài)成員函數(shù)插樁
樁函數(shù):
int fake_static_member() {
//靜態(tài)函數(shù)不需要this指針
return 300;
}
插樁示例:
assert(A::static_member(200) == 200);
mock(&A::static_member, fake_static_member);
assert(A::static_member(100) == 300);
reset();
assert(A::static_member(200) == 200);
對虛函數(shù)插樁
樁函數(shù):
int fake_virtual_member(A *pThis) {
//虛函數(shù)同普通的成員函數(shù)由于,同樣需要this指針
return 500;
}
插樁示例:
A a;
assert(a.virtual_member() == 400);
//虛函數(shù)mock需要多傳一個相關(guān)類的對象,任意一個對象即可,跟實際代碼中的對象沒有關(guān)系
A a_obj;
mock(&A::virtual_member, fake_virtual_member, &a_obj);
assert(a.virtual_member() == 500);
reset();
assert(a.virtual_member() == 400);
對系統(tǒng)及第三方庫函數(shù)插樁
樁函數(shù):
int fake_write(int, char*, int) {
return 100;
}
插樁示例:
//直接寫入一個無效的文件描述符,會失敗
assert(write(5, "hello", 5) == -1);
//來一個假的wirte
mock(write, fake_write);
//模擬調(diào)用成功
assert(write(5, "hello", 5) == 100);
reset();
assert(write(5, "hello", 5) == -1);
可以看到,對系統(tǒng)函數(shù)的 mock,其實跟普通的全局函數(shù)并無兩樣,第三方庫函數(shù)也是同理。
使用限制&注意事項
目前支持 X86_64 平臺上的 Linux、MacOS 系統(tǒng),如有需求,Windows 和其它硬件平臺,如 X86_32、ARM,也可在短期內(nèi)支持。 MacOS 下,需要在執(zhí)行前對單測可執(zhí)行文件做以下修改:
printf '\x07' | dd of=<ut_executable> bs=1 seek=160 count=1 conv=notrunc
顯然,這種方法對內(nèi)聯(lián)函數(shù)無效,不過對于單元測試來說,可以關(guān)閉內(nèi)聯(lián),同時也建議關(guān)閉其它編譯器優(yōu)化。 可以使用-fno-access-control 編譯你的測試代碼,可以使 g++關(guān)閉 c++成員的訪問控制(即 protected 及 private 不再生效)。
項目地址
https://github.com/wangyongfeng5/lmock
結(jié)語
持續(xù)改進是研效工具平臺發(fā)展的必經(jīng)之路,歡迎感興趣的同學(xué)與我們交流探討,共同助力測試效能的優(yōu)化。
