轉帖|對比評測|編輯:龔雪|2016-06-02 14:39:12.000|閱讀 1853 次
概述:統計證明,在整個軟件開發生命周期中,30%至70%的代碼邏輯設計和編碼缺陷是可以通過靜態代碼分析來發現和修復的。 本文中,將對C++代碼質量掃描主流工具進行深度對比。
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
相關鏈接:
靜態代碼分析是指無需運行被測代碼,通過詞法分析、語法分析、控制流、數據流分析等技術對程序代碼進行掃描,找出代碼隱藏的錯誤和缺陷,如參數不匹配,有歧義的嵌套語句,錯誤的遞歸,非法計算,可能出現的空指針引用等等。統計證明,在整個軟件開發生命周期中,30%至70%的代碼邏輯設計和編碼缺陷是可以通過靜態代碼分析來發現和修復的。
在C++項目開發過程中,因為其為編譯執行語言,語言規則要求較高,開發團隊往往要花費大量的時間和精力發現并修改代碼缺陷。所以C++靜態代碼分析工具能夠幫助開發人員快速、有效的定位代碼缺陷并及時糾正這些問題,從而極大地提高軟件可靠性并節省開發成本。
C/C++代碼審查工具Parasoft C/C++test
靜態代碼分析工具的優勢:
目前市場上的C++靜態代碼分析工具種類繁多且各有千秋,本文將分別介紹TSC團隊自主研發的tscancode工具和當前4種主流C++靜態代碼分析工具(cppcheck、coverity、clang、pclint),并從功能、效率、易用性等方面對它們進行分析和比較,以期幫助C++開發人員更清晰靜態代碼分析工具的工作效果、適用場景和擴展空間,同時在其對應項目特征中選擇合適的工具應用到項目開發環節中。
以下為工具在付費價格、規則數量、準確率、掃描效率、編譯依賴、IDE支持、跨平臺支持、可擴展開發方面的對比數據。注:本次競品分析的選擇了3款游戲項目(約500萬行代碼)。
在可擴展性上,TSC有專人維護,定期根據用戶需求擴展規則或新增功能特性,cppcheck和clang是開源工具,工具更新較慢,但如果用戶有特殊需求可以自己擴展開發,pclint和coverity是商業軟件,難以進行功能擴展。
同時,TSC有完整代碼質量管理閉環平臺QOC支持;coverity和clang可用web端的結果展示,但無法自行管理問題流,需要進行二次開發;cppcheck和pclint缺少web端結果展示。
以下重點比較具體檢查規則和有效問題報錯率。
針對業內大量掃描工具在實際項目中掃描結果的影響比較,我們將代碼質量問題分為以下幾大類:
根據3大影響分類,其嚴重程度分別為高、中、低,各類型規則數量分布為:
從規則分類占比來看:
整體規則數量上:pclint[915]>coverity[515]>cppcheck[245]>clang[74]>TSC[67]
可以看出pclint和coverity規則最多,TSC和clang規則最少,原因有如下3點:
注:規則總數指工具所有的規則總數,報錯規則數指開啟工具所有規則情況下,掃描樣本代碼所覆蓋的規則數量。
從實際項目掃描結果來看:
掃描出問題的規則數/規則總數:
TSC[60%]>cppcheck[27%]>clang[19%]>coverity[10%]>pclint[9%]
pclint、coverity、cppcheck雖然規則數量很多,但因為其定制加入的大部分規則普遍適用度不高,大量規則可能在多個項目中都無法掃描出問題。有些規則卻在多個項目中掃描出大量非核心的問題,如:函數沒有被調用、未使用的變量、存在多余的頭文件等。
通過對具體規則進行分析,發現在規則劃分粒度由細到出排序為[pclint,coverity,cppcheck,clang,TSC]
pclint和coverity劃分粒度最細,cppcheck,clang次之,TSC最粗。
例如:coverity的除0報錯分為整型除0,浮點數除0,取模除0;數組下標越界也細分為訪問越界、讀越界、寫越界。Pclint和cppcheck初始化分為變量未初始化、結構體成員未初始化、類成員未初始化、string未初始化、data未初始化、union未初始化、全局靜態變量未初始化等;而TSC則合并了一些過細的規則,未初始化上只分為變量未初始化和成員未初始化。
粒度劃分越細既有優點也有缺點:
優點:可以針對細分規則靈活配置開關,關掉準確率低的規則
缺點:規則數量太多, 用戶配置相當麻煩,新用戶很難理解多個相似的規則之前的區別。
TSC為降低用戶配置難度,在規則粒度劃分上相對粗獷,但會從中提取出其中準確率低的場景,作為單獨規則,從而達到可以關掉低準確率規則的目的。
本文針對每個工具在關鍵報錯項,如:空指針、越界、變量未初始化、內存泄露、邏輯上的報錯結果進行分析。
樣本代碼——3款游戲項目(約500萬行代碼)代碼
測試對象——tscancode2.0、coverity7.5、cppcheck1.68、pclint9.0、clang3.4
有效報錯數——某類規則在3款游戲項目的有效報錯數總和
準確率——某類規則在3款游戲項目的平均準確率,準確率=有效報錯數/報錯總數*100%
綜合評分——綜合有效報錯數和準確率的評分,有效報錯數和準確率的權值暫定為45:55,綜合評分=有效報錯/最大有效報錯數*100*45%+準確率*100*55%
空指針檢查規則主要檢查是否存在對賦值為空的指針解引用的情況,空指針是c/c++中最大的問題,經常造成程序崩潰的致命錯誤。因此,C++靜態代碼分析工具對空指針的檢查能力顯得尤為重要。
圖為五個工具對樣本代碼掃描結果:
有效報錯數:TSC [401] >coverity[219]>>clang[57] >cppcheck[20]>pclint[14]
準確率:coverity[95%]≈TSC[92%] ≈clang[90%]>>cppcheck[28%]>pclint[14%]
綜合評分:TSC[96分] >coverity[77分] >clang[56分]>cppcheck[18分]>pclint[8分]
cppcheck掃描出來的問題存在大量誤報,誤報主要是冗余的判空,并不會引起實際問題,具體誤報場景如下:
越界一般來講是指數組下標越界,或者緩沖區讀寫越界。這類錯誤會導致非法內存的訪問,引發程序崩潰或者錯誤。
下圖是五個工具對樣本代碼掃描結果:
注:越界對誤報判定的規則比較嚴格,即使場景識別本身無誤,但是通過代碼邏輯可以推斷該場景不會越界的也判定為誤報。
例如:
這里由found變量間接推斷出data[region_index]不會越界,將其判定為誤報。
從報錯數量和準確率來看:
有效報錯數:coverity[98]>>TSC [18]>pclint[16] >cppcheck[6]> clang[4]
準確率:clang[100%] >coverity[80%]>TSC[70%] >cppcheck[67%]>>pclint[2%]
綜合評分:coverity[90分] >TSC[54分]≈clang[55分]>cppcheck[40分]>pclint[1分]
對于數組下標iCountry的判定存在風險,代碼執行到當前上下文時,iCountry可能 取值為MAX_QT_COUNTRY_JIFEN_ITEM_CNT,而這正是數組m_astDataInDB的長 度,也就是說在這種邊界情況下會造成了數組訪問越界。對于如上場景,應該將代碼修 改為iCountry>= MAX_QT_COUNTRY_JIFEN_ITEM_CNT。
變量未初始化顧名思義:變量聲明后沒有賦初值,其分配的內存值是隨機的。這也是代碼中容易出現的問題,會導致不確定的程序行為,造成嚴重的后果。
下圖是五個工具對樣本代碼掃描結果:
注:結果排除了3個工具都有的檢查項——構造函數中是否存在未初始化成員變量。在實際項目中發現,C++類構造函數中對成員變量不做初始化的情況是普遍的,很多代碼會采用“延遲初始化”,即在實際用到該對象的時候調用類似Initialize的方法進行初始化。因此在此次對比中并沒有把這條規則納入進來。
從報錯數量和準確率來看:
有效報錯數:coverity[75]>>pclint[25] >TSC [9]>cppcheck[8]> clang[1]
準確率:TSC[75%] >coverity[68%]>pclint[26%] > clang[17%] >cppcheck[3%]
綜合評分:coverity[82分] > TSC[47分] >pclint[30分] > clang[10分] >cppcheck[6分]
SMD_POS是一個簡單的結構體,它包含了一個空的構造函數,cppcheck依據這點 判定這是一個未初始化的錯誤。但這樣的場景不會有什么問題,算是一個誤報。這導致 了cppcheck在未初始化規則的結果可信度大大降低。
TSC在未初始化變量的檢查因不具備路徑分析能力,而以分支作用域檢查特定變量 在各個代碼分支的初始化情況,誤報率保持在相對低的一個水平。但場景覆蓋較少,沒 有針對結構體字段的初始化場景做覆蓋。因為對結構字段的初始化方式相對比較多樣: 逐個字段初始化,函數調用初始化,構造函數初始化等。
內存泄漏指由于疏忽或錯誤造成程序未能釋放已經不再使用的內存,從而造成了內 存浪費的情況。內存泄漏是靜態下很難檢測的一種錯誤,一般需要動態分析工具進行檢 測,如valgrind工具會捕獲malloc()/free()/new/delete的調用,監控內存分配和釋放,從 動態上檢測程序是否存在內存泄漏。因此,靜態代碼分析能檢查的內存泄漏就非常有限 了,當前各工具主要是從代碼寫法上檢查內存分配和釋放是否配對使用。比如:fopen 打開文件后在退出函數前是否有執行fclose,new[]和delete[]是否配對使用等。
下圖是五個工具對樣本代碼掃描結果:
注:以上數據排除了cppcheck35個低價值報錯,這里排除的cppcheck35個報錯都是基本數據類型的new和delete不匹配(如char* p=new char[100];delete p;)雖然這種寫法不規范,但由于實際上不會造成內存泄漏,很多項目不會對此進行修復。
從報錯數量和準確率來看:
有效報錯數:pclint[55] >TSC[40]>coverity [29]>cppcheck[28]> clang[0]
準確率:coverity[100%]=cppcheck[100%] >TSC[73%]>pclint[23%] > clang[N/A]
綜合評分:coverity[79分] ≈ TSC [73分]≈cppcheck[77分]>pclint[57分]>clang[0分]
從報錯數量上看出,在內存泄漏檢查方面,pclint雖然發現有效問題最多,但誤報很高,不推薦使用。TSC的有效錯誤數比coverity和cppcheck多,但誤報也相對較高。clang則不具備泄露類場景的檢測能力。
注:由于靜態掃描能檢查的內存泄露場景都非常明確,因此一般都不會出現問題,TSC的15個誤報也非場景識別有誤而是工具底層bug導致,后續會對底層bug進行修復。如:#ifdef 和#else分支中各有一個fopen,實際編譯時只會走其中1個分支識別1次fopen,但由于底層bug識別了2次fopen,導致誤報。
邏輯錯誤:指可能存在的邏輯問題,如if不同分支內容相同,在switch內缺少break等,對指針使用sizeof進行空間分配等問題。
下圖是五個工具對樣本代碼掃描結果:
注:這些報錯中剔除了一些無修改意義且結果數量很多規則:如:coverity掃描存在7484條Logically dead code(邏輯代碼不可達)報錯。cppcheck存在2246條unusedFunction(函數未被使用)報錯。
從報錯數量和準確率來看:
有效數量:TSC[293]>coverity[164]>clang[142] >cppcheck [120]>pclint[116]
準確率:clang[97%] >TSC[93%]>coverity(88%)>pclint[72%] >cppcheck[55%]
綜合評分:coverity[94分] > TSC[86分] > clang[80分] >cppcheck[63分] >pclint[27分]
從報錯數量和準確率上可以看出TSC可以更有效的發現邏輯類問題。但各工具邏輯類場景各有特色,互為互補,可以一同選擇掃描,但cppcheck和pclint準確率較低,可以較少選擇。clang的準確率最高,但clang掃描出來的邏輯錯誤中有一大半為低價值的邏輯錯誤,比如clang掃描出來的142條邏輯錯誤中就有140條“變量賦值但沒有使用”錯誤。
①TSC,coverity具備較強宏展開能力
以DuplicateExpression規則為例,TSC發現DuplicateExpression規則報錯32條,cppcheck發現DuplicateExpression規則報錯12條。因為TSC可以對宏進行更有效展開,例如:
這種報錯TSC可以準確的識別出來,宏MAX_TASK_TAB_SIZE和MAX_TASK_RES_NUM為相同的數值,而cppcheck無法區分發現這類問題,只能進行簡單的文本匹配。coverity在推斷能力上也不差,在這點也明顯優于cppcheck。
②TSC規則類型更有效
經過篩選,TSC只保留價值更高的推斷和有效規則;
Ø增加一些函數檢查規則,如:MemsetZeroBytes,這種錯誤的Memset寫法:memset(ctYear, sizeof(ctYear),0);可疑的數組下標使用等這些規則在coverity邏輯類檢查中并沒有體現,而coverity只會報出非常準確的報錯如:if分支完全相同等檢查項。
Ø剔除價值低的無效規則,如coverity規則Logically dead code,指一些邏輯上不可達的廢棄代碼;cppcheck規則memsetClassFloatc指對存在Float類型成員變量的Class
使用Memset,當時代碼中發現基本都是Memset為0,并不會有數據丟失等問題。故這類規則發現有效問題很低,在數量較大的情況下,需要耗費大量的人力來確認,性價比不高,TSC已經將這種規則剔除。
總的來說,TSC在發現問題和準確率方面表現都不錯,可以節省大量的人力在鎖定邏輯類型錯誤。
TSC在某些細小規則的推斷能力上比coverity要稍微弱一些,如規則Missing break in switch:coverity發現全部準確的報錯,TSC存在一定的誤報,這些復雜場景需要較強的動態計算如:
誤報場景一(cppcheck)
以上538行代碼報quiz_set_ptt存在空指針訪問。
誤報原因:538行只是指針的比較,并沒有解引用,這是一個比較低級的誤報。
誤報場景二(coverity)
以上119行代碼報actor存在空指針訪問,判定邏輯如下:112行對actor進行了判空,說明actor在當前上下文可能為空。所以119行actor可能為空。
誤報原因:xy_assert_retval是個宏,展開后包含有return語句,即如果actor為空115行就返回了,119行actor不會為空。
誤報場景一(TSC)
以上83行代碼報第數組訪問可能越界,判定邏輯如下:第61行的if語句對req_list.num的取值范圍作了限制,req_list.num在當前上下文的最大值可以是
MAX_RECRUIT_REQ_LIST_SIZE(4);83行req_list._數組對象用req_list.num作為其數組訪問的下標,當req_list.num取值為MAX_RECRUIT_REQ_LIST_SIZE時發生越界(req_list._數組的長度為MAX_RECRUIT_REQ_LIST_SIZE(4))。
誤報原因:第79行的if條件保證了之后的代碼req_list.num的值不會等于MAX_RECRUIT_REQ_LIST_SIZE,所以這是一個誤報。
誤報場景二(cppcheck)
以上第691行代碼報t_index_map可能取值-1越界,判定邏輯如下:665行聲明t_index_map并賦值為-1,t_index_map的賦值在681行,但681行在for循環里面,而for循環存在不能進入的可能性,所以在691行使用t_index_map可能未初始化。
誤報原因:進入691行代碼的前提條件是found變量為true,而found為true保證了t_index_map被賦值了。
誤報場景三(coverity)
以上第146行代碼報src_index + 1可能取值為4越界,判定邏輯如下:139行對src_idx的取值范圍進行了限定:[0, 3](TEAM_MEMBER_MAX長度為4),因此146行src_idx + 1可能為4導致對team_ptr->team_member訪問越界。
誤報原因:144行對src_idx的取值范圍進行了過濾,保證了src_idx+1不會越界。
誤報場景一(cppcheck)
以上第462行代碼報ret未初始化錯誤,判定邏輯如下:ret變量在第434行聲明,在switch中的兩個case中均有初始化代碼,但是在default分支中沒有對ret進行初始化,因此判定462行可能會返回一個沒有初始化的ret。
誤報原因:default分支中的xy_assert_retval是一個宏,因為cppcheck宏查找策略的原因導致該宏沒有展開。實際上宏展開包含了return語句,也就是說如果進入default分支就函數就直接返回而不會執行到462行代碼。
誤報場景二(coverity)
以上第284行代碼報careers未初始化錯誤,判定邏輯如下:careers數組在第278行聲明,但在for循環對每個數組成員進行了初始化。這可能造成careers完全沒有初始化,或者只初始化了一部分。因此在284行使用careers存在未初始化錯誤。
誤報原因:通過代碼邏輯可知,career_num代表的是careers被初始化的長度,在訪問careers數組元素的時候,通過career_num進行了保護,因此不會出現未初始化的錯誤。
誤報場景一(TSC)
以上第63行代碼報fp存在資源泄露風險錯誤,判定邏輯如下:xy_assert_retnone宏展開后,含有return語句,也就是說fp在調用fclose之前可能返回,存在泄露風險。
誤報原因:實際上代碼邏輯決定了函數return的前提條件fp為空。這個時候是沒有必要調用fclose的,不存在泄露風險。
誤報場景二(pclint)
以上第139行代碼(~CGIProcessor(), 析構函數)報存在資源泄露風險錯誤,因為沒有釋放_cgiContainer。判定邏輯如下:_cgiContainer作為CGIProcessor的一個指針成員(第149行),需要在析構函數中進行釋放,否則為內存泄露。
誤報原因:CGIProcessor對象并不own _cgiContainer指向的對象,不需要它來釋放。
誤報場景一(cppcheck)
以上4596行代碼報“對包含有float成員的對象調用memset方法”錯誤。
誤報原因:利用memset對一個對象的數據字段清零是比較常見的做法,float成員清零后值也為0,不會造成什么問題。
原文轉載自:
本站文章除注明轉載外,均為本站原創或翻譯。歡迎任何形式的轉載,但請務必注明出處、不得修改原文相關鏈接,如果存在內容上的異議請郵件反饋至chenjj@fc6vip.cn