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