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