转帖|对比评测|编辑:龚雪|2016-06-02 14:39:12.000|阅读 1773 次
概述:统计证明,在整个软件开发生命周期中,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@pclwef.cn