源码先锋

源码先锋

C++代码质量扫描主流工具深度比较

admin 120 173

本文由腾讯WeTest团队提供,更多资讯可直接戳链接查看:

引言静态代码分析是指无需运行被测代码,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,找出代码隐藏的错误和缺陷,如参数不匹配,有歧义的嵌套语句,错误的递归,非法计算,可能出现的空指针引用等等。统计证明,在整个软件开发生命周期中,30%至70%的代码逻辑设计和编码缺陷是可以通过静态代码分析来发现和修复的。

在C++项目开发过程中,因为其为编译执行语言,语言规则要求较高,开发团队往往要花费大量的时间和精力发现并修改代码缺陷。所以C++静态代码分析工具能够帮助开发人员快速、有效的定位代码缺陷并及时纠正这些问题,从而极大地提高软件可靠性并节省开发成本。

静态代码分析工具的优势:

1.自动执行静态代码分析,快速定位代码隐藏错误和缺陷。

2.帮助代码设计人员更专注于分析和解决代码设计缺陷。

3.减少在代码人工检查上花费的时间,提高软件可靠性并节省开发成本。

2业界主流静态代码扫描工具概况目前市场上的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检查规则大比拼

3.1规则大类

针对业内大量扫描工具在实际项目中扫描结果的影响比较,我们将代码质量问题分为以下几大类:

①致命类:可能导致程序宕机、无响应等影响范围极大的错误;

②逻辑类:可能造成程序不能达到预期逻辑结果的错误;

③编码规范及其他类:可能造成程序的可读性、可维护性较差的错误(不可达代码,无效的变量声明等);

3.2规则大类分布

根据3大影响分类,其严重程度分别为高、中、低,各类型规则数量分布为:

从规则分类占比来看:

②coverity作为商业化软件,在付费后添加规则上,达到覆盖率最全面,除致命和逻辑类规则外,还有大量编码规范、安全和针对其他语言(如java,Cifdef和#else分支中各有一个fopen,实际编译时只会走其中1个分支识别1次fopen,但由于底层bug识别了2次fopen,导致误报。

4.5逻辑错误规则逻辑错误:指可能存在的逻辑问题,如if不同分支内容相同,在switch内缺少break等,对指针使用sizeof进行空间分配等问题。

下图是五个工具对样本代码扫描结果:

注:这些报错中剔除了一些无修改意义且结果数量很多规则:如:coverity扫描存在7484条Logicallydeadcode(逻辑代码不可达)报错。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条“变量赋值但没有使用”错误。

1.TSC,coverity具备较强宏展开能力

以DuplicateExpression规则为例,TSC发现DuplicateExpression规则报错32条,cppcheck发现DuplicateExpression规则报错12条。因为TSC可以对宏进行更有效展开,例如:

这种报错TSC可以准确的识别出来,宏MAX_TASK_TAB_SIZE和MAX_TASK_RES_NUM为相同的数值,而cppcheck无法区分发现这类问题,只能进行简单的文本匹配。coverity在推断能力上也不差,在这点也明显优于cppcheck。

2.TSC规则类型更有效

经过筛选,TSC只保留价值更高的推断和有效规则;

Ø增加一些函数检查规则,如:MemsetZeroBytes,这种错误的Memset写法:memset(ctYear,sizeof(ctYear),0);可疑的数组下标使用等这些规则在coverity逻辑类检查中并没有体现,而coverity只会报出非常准确的报错如:if分支完全相同等检查项。

Ø剔除价值低的无效规则,如coverity规则Logicallydeadcode,指一些逻辑上不可达的废弃代码;cppcheck规则memsetClassFloatc指对存在Float类型成员变量的Class使用Memset,当时代码中发现基本都是Memset为0,并不会有数据丢失等问题。故这类规则发现有效问题很低,在数量较大的情况下,需要耗费大量的人力来确认,性价比不高,TSC已经将这种规则剔除。

总的来说,TSC在发现问题和准确率方面表现都不错,可以节省大量的人力在锁定逻辑类型错误。

TSC在某些细小规则的推断能力上比coverity要稍微弱一些,如规则Missingbreakinswitch:coverity发现全部准确的报错,TSC存在一定的误报,这些复杂场景需要较强的动态计算如:

5常见误报场景

5.1空指针常见误报场景

误报场景一(cppcheck)

以上538行代码报quiz_set_ptt存在空指针访问。

误报原因:538行只是指针的比较,并没有解引用,这是一个比较低级的误报。

误报场景二(coverity)

以上119行代码报actor存在空指针访问,判定逻辑如下:112行对actor进行了判空,说明actor在当前上下文可能为空。所以119行actor可能为空。

误报原因:xy_assert_retval是个宏,展开后包含有return语句,即如果actor为空115行就返回了,119行actor不会为空。

5.2越界常见误报场景

误报场景一(TSC)

以上83行代码报第数组访问可能越界,判定逻辑如下:第61行的if语句对req_的取值范围作了限制,req_在当前上下文的最大值可以是MAX_RECRUIT_REQ_LIST_SIZE(4);83行req_list.数组对象用req_作为其数组访问的下标,当req_取值为MAX_RECRUIT_REQ_LIST_SIZE时发生越界(req_list.数组的长度为MAX_RECRUIT_REQ_LIST_SIZE(4))。

误报原因:第79行的if条件保证了之后的代码req_的值不会等于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,因此146行src_idx+1可能为4导致对team_ptr-team_member访问越界。

误报原因:144行对src_idx的取值范围进行了过滤,保证了src_idx+1不会越界。

5.3未初始化常见误报场景

误报场景一(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进行了保护,因此不会出现未初始化的错误。

5.4泄露类常见误报场景

误报场景一(TSC)

以上第63行代码报fp存在资源泄露风险错误,判定逻辑如下:xy_assert_retnone宏展开后,含有return语句,也就是说fp在调用fclose之前可能返回,存在泄露风险。

误报原因:实际上代码逻辑决定了函数return的前提条件fp为空。这个时候是没有必要调用fclose的,不存在泄露风险。

误报场景二(pclint)

以上第139行代码(~CGIProcessor,析构函数)报存在资源泄露风险错误,因为没有释放_cgiContainer。判定逻辑如下:_cgiContainer作为CGIProcessor的一个指针成员(第149行),需要在析构函数中进行释放,否则为内存泄露。

误报原因:CGIProcessor对象并不own_cgiContainer指向的对象,不需要它来释放。

5.5逻辑类常见误报场景

误报场景一(cppcheck)

以上4596行代码报“对包含有float成员的对象调用memset方法”错误。

误报原因:利用memset对一个对象的数据字段清零是比较常见的做法,float成员清零后值也为0,不会造成什么问题。