业界主流C#静态代码扫描工具大比拼

本文将分别介绍TscSharp工具(简称TSC#)和3款主流C#静态代码分析工具 (coverity、resharper、gendarme)

一、TSC#诞生记
相信了解过c++静态代码扫描的同学一定对TscanCode(简称TSC)不会陌生,目前公司各BG已经有上百个项目接入了TSC代码扫描平台。随着公司越来越多unity引擎手游项目的出现,C#代码扫描的需求也越来越多,为此我们自主研发了针对C#语言的扫描工具TscSharp(简称TSC#)。

由于C#和C++在语法特性和编译方式都存在一些细节差异,因此,我们无法直接沿用TSC的底层实现,而是针对C#的语法特性和编译方式做了全新的底层架构。下面我们来看看C#和C++在语法特性和编译方式上的差异。


1.1 C#与C++语法特性差异

C#和C++都是类C语言,在很多方面极为相似。但从细节来看,两种语言的差异还是很明显,这些差异会导致工具在底层词法分析和语法分析上的不同,TSC#会增加若干在新增语言词法语法特性的特征集合和代码场景特征。例如C#包含属性(Property),特性(Attribute),泛型(Generic),索引器(Indexer),委托(Delegate),事件(Event),Lambda,Linq,??操作符,动态语言,checked/unchecked/using/unsafe关键字等特有的语法特性。


1.2 编译方式差异
C++使用include进行跨文件符号查找,而C#用的是using。
C++在编译的时候,针对每个cpp文件,会递归的展开它include的所有头文件,这个操作可以保证cpp文件中所有符号的声明都是可以找到的(定义可以没有)。

C#只有*.cs文件,相当于把.h和.cpp文件合在一起了。再加上C#文件不关心文件,只关心引用的命名空间。很多C#的整个项目都只有一个命名空间,因此我们根据C#特定设计了一套全新的扫描模型,在确保符号查找完备的同时保证扫描效率。


二、业界发展情况
目前市场上的C#静态代码分析工具相较于C++较少,本文将分别介绍TSC团队自主研发的TscSharp工具(简称TSC#)和3款主流C#静态代码分析工具 (coverity、resharper、gendarme),并从扫描规则、结果有效性、效率、易用性等方面对它们进行分析比较,让C#开发和测试同学更清晰C#静态代码分析工具的适用场景和实际效果,以便根据自身项目特征和开发阶段和节奏,选择适合的工具应用到项目中,更好的保证项目的品质。
首先为工具在整体上的一个比对,依次从付费价格、扫描对象、扫描效率、已有规则数量、编译依赖性、IDE插件支持度进行数据比对。

1、从实际工具的试用结果来看,和C++代码扫描工具的结果规则严重影响的类型定义程度上大体是一致的,也分为:空引用类(如空引用异常)、一般逻辑类(如String.Format参数错误,奇数计算方法对负数不生效),其他类(如避免不必要的拆箱操作),TSC#、resharper、gendarme和coverity这4个工具中包括第一类和第二类的问题的规则,这些规则如果不满足一般会引起程序崩溃、无响应或逻辑错误,是我们重点关注的,本文会详细针对这4个工具的具体结果做分析。而Fxcop和stylecop这两个工具,大家如果有了解的话,在C#领域基本全是编码规范类规则,此类问题一般只影响代码的可读性,互联网相关容错较多的行业的开发同学一般没太多时间来关注,因此全面对比中排除了这两个工具。
2、从检查对象来看,除gendarme外,其他工具检查的对象都是项目源码。gendarme是检查编译好的assembly文件(exe或dll),每个assembly都有其metadata(可称为元数据,metadata是关于一个assembly中各元素的类型信息库,它本身也存放在这个assembly中),它对assembly以及assembly内用到的所有类型进行描述。即工具通过mono.cecil库来获取assembly文件中各个模块、函数、类型的中间代码,通过扫描中间代码来间接对源码进行检查,这样的好处是不依赖源码,但有个比较大的问题就是必须提供对应版本的符号文件,否则无法定位到文件行,并且即使提供了符号文件,也有部分错误无法精准定位。
3、从扫描效率上来看TSC#和gendarme效率最高,平均10万行代码只需10秒就能扫描完毕,resharper效率居中,扫描耗时约为TSC#和gendarme的2倍,coverity因为基于编译,扫描效率最慢,扫描耗时就是TSC#的9倍。
注:以上扫描耗时并没加上编译时长,由于coverity依赖编译,若加上编译时长,coverity工具的整体扫描耗时会明显增加,coverity扫描效率将由77秒/10万行增加到110秒/10万行。
4、在平台支持和结果管理方面,TSC#有完整代码质量管理闭环平台支持;coverity可用web端展示结果,但无法自行管理问题流,需要进行二次开发;resharper、gendarme、Fxcop和stylecop缺少web端结果展示。

以下重点比较各个工具检查规则和有效问题报错率。


三、工具规则大比拼
3.1规则大类
参考C++对应的规则结果影响,和现有TSC#已经开发的规则类别,以及这几个重点C#扫描工具在实际项目中扫描结果的影响比较,将C#代码质量问题分为以下几大类:
1) 致命类:导致空引用异常,造成程序崩溃、无响应等影响范围极大的错误;
注:C#扫描工具中基本没有初始化规则,因为C#中静态变量、数组元素都有默认初始值,无需初始化,并且对于其他需要进行初始化的变量,没有初始化的话,编译器本身会做检查、无法通过编译。
2)逻辑类:可能造成程序不能达到预期逻辑结果的错误;
3)Unity特性类:Unity引擎特性相关的一些规则,如继承于MonoBehaviour的类不应该自己实现构造函数;
4)其他类:可能造成程序的可读性、可维护性较差的错误(不可达代码,无效的变量声明等);

resharper、gendarme和coverity的规则都覆盖了除unity特性外的其他三类规则,但TSC#为了保障解决项目核心问题,和减少规则筛选对使用者带来的不便,定位于真正会引起程序崩溃或逻辑错误的致命类、逻辑类规则以及unity特性类规则,当然更多功能也可以灵活的实现规则增加和优化。


3.2 有效规则和报错数量
以2款游戏(涉及到代码保密原则)70万行代码扫描结果为例,对各工具有效规则和报错数量进行分析。

3.2.1 规则数
注:规则总数指工具包含的所有规则数量;报错规则数指在扫描样本代码时工具真正报错的规则数量;有效规则数指所有报错的规则中我们认为有效的规则数量。
①    规则总数:
resharper[1400]>coverity[515]>gendarme[256]>TSC#[25]
从工具规则总数来看,resharper和coverity覆盖的规则最全,但绝大部分规则在实际项目中都没有扫描出错误,TSC#规则最少,但大部分规则都能在实际项目中扫描出错误。
resharper和coverity作为商业化软件,支持的语言较多,如resharper支持C#, VB.NET,  XML, ASP.NET, JavaScript, TypeScript, HTML等代码扫描,而coverity支持C++,C#,java代码扫描,因此其规则数量众多,但很多规则可能只支持某种语言,在另一种语言下规则不生效,但其在配置选择上却没有区分开,导致规则的通用度不高,98%的规则在多个C#项目中都无法扫描出问题。
例如:coverity规则中宏里面的无符号数与0的比较(Macro compares unsigned to 0)对C#代码是无效的,因为C#语法特性决定了C#中的宏只能做条件编译,不可能存在逻辑代码;变量未初始化检查对C# 代码也是无效的,C#中静态变量、数组元素都有默认初始值,无需初始化,并且对于其他需要进行初始化的变量,没有初始化的话,编译器本身会做检查、无法通过编译。
 
②     报错规则数:
resharper[135]>gendarme[107]> TSC#[16]>coverity[10]
有效规则数:TSC#[16]= resharper[16]>coverity[10]>gendarme[7]
从实际项目扫描结果可以看出,resharper、gendarme的报错规则数虽然有100余条,但90%+都是编码规范类的较低价值问题,比如:函数命名不符合规范、使用非字母数字的标识符、太长的函数体等,真正有效的报错规则仅有16条和7条。
综上所述,虽然商业软件覆盖规则最为全面,但在实际项目应用中其有效性可能并不高。从实际游戏项目扫描结果来看,在有效规则数上,TSC#和resharper都有16条规则,略优于coverity,gendarme,后两者有效规则分别为10条和7条。

 

3.2.2 报错数
下面从实际游戏项目报错数来对各工具进行分析。

报错总数:
resharper[157819]>gendarme[77231]>TSC#[337]>coverity[274]
有效规则报错数:
TSC#[337]>coverity[274]>resharper [154]>gendarme[72]
通过过滤分析问题知道,TSC#和coverity在有效报错数上明显优于resharper和gendarme。TSC#和coverity报错总数虽然最少但报出来的问题基本是有效问题,而resharper和gendarme报错总数最多,分别高达15.7万和7.7万,但99%都是低价值的编码规范类问题,有效错误数不到总报错数的1%,程序如果要筛选过滤这部分低价值问题其实是非常耗时低效的。


四、同类规则效果对比分析
本节针对每个工具在有效报错项:空引用、逻辑、Unity特性、编码规范和其他类的报错结果进行分析。
测试对象——TSC#、coverity、resharper、gendarme文章管理
有效报错数——某类规则在2款游戏项目中有效报错数总和
准确率——某类规则在2款游戏项目中的准确率,准确率=有效报错数/报错总数*100%

综合评分——综合有效报错数和准确率的评分,有效报错数和准确率的权值定为45:55,综合评分=有效报错/最大有效报错数*100*45%+准确率*100*55%


4.1致命类规则
致命类规则主要检查可能会导致程序异常甚至crash的代码问题,这类错误一旦发生,基本上都会导致程序逻辑错误,影响面大。如空引用问题。

从报错数量和准确率来看
有效数量:TSC#[275] >coverity 
[209] >resharper [40] >gendarme [11]
准确率:
TSC#[97%] >coverity[96%] > gendarme[92%]>resharper[68%]
综合评分:TSC#[98] >coverity[87] >gendarme[51]>resharper[44]
从数量上看,TSC#和coverity保持在相同的量级,对C#空引用检查都具备较好的推断分析能力。Gendarme发现的问题数量比较少,但是空指针规则CallingEqualsWithNullArgRule准确率不错,能够帮助用户快速定位一些有价值问题。Resharpe准确率比较于其他三个工具要弱的多。在空引用场景细化程度上,TSC#细化了报错规则,分为:
l  判空前解引用[CS_dereferenceBeforeNullCheck],
l  函数返回值为空[CS_FuncRetNull]
l  先判空再解引用但是此时判空已经失效[CS_dereferenceAfterNullCheck]
l  判空的情况下直接解引用[CS_dereferenceIfNull]
类似coverity拥有NULL_RETURNS,FORWARD_NULL等细分。
从准确率上看,TSC#和coverity准确率较好,大概在96%左右。resharper因为规则粒度较大,空引用类型都归类为PossibleNullReferenceException,其中存在一定比例的误报不能通过规则开关来控制,将要消耗大量人力去过滤,如下三元表达式的空引用误报,在eggSlots为空情况下,并不会执行eggSlot.Length,没有空引用隐患。

gendarme因为数量较少,准确率参考性不强。

综上,TSC#和coverity在空引用检查方面有较好的规则细分,数量上二者保持基本量级,准确率方面TSC#,coverity和gendarme都在90%以上。在误报方面,TSC#在类成员初始化后引用存在一定的误报,如图:类成员在判空后执行了Initial函数内new了新对象赋值,通常来说这种New失败的概率很小,一般不需要进行额外判空。而Coverity则在这方面会进行一定的逻辑推断,但是在一些特定逻辑分支下也会产生误报,如图:Coverity错误的判定在_setBlur(null)内发生空指针解引用,arg=null时,通过_blurArg逻辑可以有效保证arg.times不会被执行到。

4.2逻辑错误规则
逻辑错误:指可能存在的逻辑问题,如对C#调用StringFormat不合理导致Crash的检查,CS_VirtualCallInConstructo在构造函数中调用虚函数等,表示式恒为True或者False等。
下图是四个工具对样本代码扫描结果的报错数量统计分析:

从报错数量和准确率来看
有效报错数量:
TSC#[68] >coverity[50] >resharper[42] >gendarme[11]
准确率:
TSC#[94%] >coverity[89%] >resharper[63%] > gendarme[28%]
综合评分:TSC#[96] >coverity [82] >resharper[62] > gendarme[22]
从数量上看,TSC#具有最多的报错,相对coverity,TSC#拥有一些C#定制的规则,如StringFormat参数检查。这种错误可能会引发C#程序抛出异常从而导致Crash。

coverity缺乏这方面规则,导致整体报错数量较TSC#少。对比另外两款C#扫描工具resharper和gendarme,TSC#各自都有一些TSC#定制的规则,但是TSC#针对规则中无效报错进行了过滤和筛选,从而比他们具备更高的准确性。
从准确率上看,TSC#有最高的准确率,对于变量和属性值的变化都有较好的推理能力,例如在“循环控制变量在循环体中未被使用”这条规则的扫描中,对象属性下标变化,rendarme和resharper都不能准确推断属性值的变化,从而产生误报。如下:

尤其是gendarme准确率跟其他工具相比要明显偏低(28%),分析发现gendarme在对“StringFormat参数个数不匹配“错误进行分析时存在较大量的误报,实际上这种写法并不会造成什么严重的后果,TSC#则是过滤掉了这种无效的报错。如下:

综上,TSC#和coverity拥有较好的逻辑推断能力。resharper和gendarme的误报率较高。通过整理四个工具的逻辑类规则,它们相互覆盖的情况如下:

如TSC#和resharper,coverity,我们对比他们之间三种类型相同的扫描规则,从规则来看,TSC#融合了Coverity中逻辑类型经典的推断规则和Resharp中C#定制的规则项,从而将会有更强的逻辑扫描覆盖。交集规则如下:

综合来说,Coverity因为缺失C#定制规则检查,如StringFormat和VirtualCallInConstructor检查中TSC#就比Coverity累计发现10个问题。而逻辑推断能力上,循环控制变量是否改变检查,resharper就对16个问题存在14个误报,推断分析误报交错。Gendarme则因为在逻辑推断和C#规则上都有较多误报评分排在最后。

 

4.3 Unity类型规则
规则描述:Unity程序在实际代码中发现有一些Unity引擎特性相关的问题。这些规则本身不是C#语法规定,但是现在越来越多手游崛起,依赖于unity项目也越来越多,对这些项目Unity特性检查可以有效帮助他们提高程序性能,避免潜在问题。如继承于MonoBehaviour的类的构造函数不应该初始化成员变量。避免大量空update()函数调用,从而提高帧率等。

如图:避免不必要的Update空函数:

TSC#针对Unity项目的特性定制了一些扫描项,可以在Unity项目中应用并且发现潜在的代码风险。其他三个工具暂时还有没有支持Unity特性的检查。目前这块规则TSC#也正在不断添加完善,期待大家帮忙提供更多新场景完善扫描。


4.4其他规则
主要包含代效率,规范规则等不会对程序逻辑正确执行产生影响的规则集合。
下图是四个工具对样本代码扫描结果:

从报错数量和准确率来看
有效数量:TSC#[54]>resharper[41]>gendarme[21] >coverity[0]
准确率:
TSC#[100%] =resharper[100%] =gendarme[100%] >coverity[0%]
综合评分:TSC#[100] >resharper[89] > gendarme[73] >coverity [0]
从数量上看,TSC#扫描错误最多,主要集中在报错规则CS_DerivedClassNewKeyWord上。new 关键字可以显式隐藏从基类继承的成员。隐藏继承的成员时,该成员的派生版本将替换基类版本。虽然可以在不使用 new 修饰符的情况下隐藏成员,但会生成警告。若要隐藏继承的成员,请使用相同名称在派生类中声明该成员,并使用 new 修饰符修饰该成员,避免产生不必要错误。例如:

对于复杂的继承关系,同名属性很可能使程序调用产生逻辑混淆。因此合理使用new关键字是有必要的。TSC#和resharperer都能在项目中发现这类问题,如当一个新的开发人员在复杂继承关系中添加了一个新的类型,并且添加了一个新属性Invoke,无意中覆盖了上层,甚至于上上层基类的属性,而其他人可能还以为值调用了父类的Invoke,为将来代码维护和扩展埋下了隐患。

gendarme表现在对不必要的拆箱装箱和性能问题有一定检查能力,减少这些不必要的Boxing可以在提升程序性能。coverity则缺失对C#项目中逻辑类,性能等问题的检查能力。综合评分看来,TSC#和resharper表现要好些。


五、常见案例
5.1空引用类型报错场景
TSC#,coverity,resharper,gendarme致命问题均为可能发生crash的空引用检查。在大的空引用报错规则上,TSC#和TSC的C++规则保持一致,会有一些细分的点。下面列举TSC#和covertiy两个空引用场景,就属于TSC#和TSC一样的,同时也和coverity同步的场景的实例。
CS_dereferenceAfterNullCheck(TSC#)
规则描述:在NULL检查之后,然后调用了空引用可能导致程序Crash。如下:

FORWARD_NULL(coverity)
规则描述:在NULL检查之后,然后调用了空引用可能导致程序Crash。如下:

 

5.2逻辑类型报错场景
对于逻辑类型场景检查,各个工具都有各自检查点,下面为从中抽选一些的规则场景。
CS_StringFormat(TSC#)
规则描述:在调用String.Format()进行字符串格式化时,传入的参数个数和格式化字符串期望的参数个数不匹配。如下:

CompareOfFloatsByEqualityOperator(resharper)
规则描述:对Float类型变量用”==”直接判等。我们知道,对于浮点数判等是有一定风险的。建议在合理的精度范围内使用绝对值差来比较。如:if( abs(f1-f2)<0.000001)
项目实际报错场景示例:

变量dir和Direction.LEFT直接判等,存在一定风险。
COPY_PASTE_ERROR(coverity)
规则描述:在同一个作用域内,出现了两个完全重复的代码段。第二段代码可能是程序错误的COPYPASTE。

ReplaceIncompleteOddnessCheckRule(gendarme)

规则描述:使用”var%2==1”对变量进行奇偶判定。当var为负奇数,var%2的值等于-1,而只是通过”==1”来判断在逻辑上是不全面的。


5.3 Unity特性报错场景(TSC#)
Unity特性场景是针对Unity编程特性做的一些检查,四个工具中,暂时只有TSC#有此类型的检查,下面为抽选的Unity特性场景。
CS_EmptyUpdate(TSC#)
规则描述:针对Unity项目代码定制的规则。Unity每一帧都会调用执行Update(),LateUpdate()等更新函数,如果存在大量空的更新函数会影响Unity性能。例如:

CS_UnsafeConstructor(TSC#)

规则描述:在Unity中继承于MonoBehaviour的子类,不应该使用构造函数来进行初始化。官方建议使用Start()或者Awake()函数进行相关成员变量的初始化。例如:


5.4其他报错场景
其他类型报错多位一些警告或者优化,从中抽选两个TSC#和gendarme报错场景进行详细描述如下。
CS_DerivedClassNewKeyWord(TSC#)
规则描述:若要隐藏继承的成员,请使用相同名称在派生类中声明该成员,并使用 new 修饰符修饰该成员。
示例如下:在子类和父类中拥有同名的函数init,建议应该在子类init函数之前添加new关键字修饰来区别于父类init函数。

AvoidUnneededUnboxingRule(gendarme)
规则描述:在C#中,过多的装箱拆装箱操作会产生相对较大的性能消耗,官方建议应该尽量减少不必要的拆装箱操作。
示例如下:在循环中出现了重复的(MailType)Value拆箱操作,存在优化空间。


工具即将在今年下班年登陆WeTest哦,届时大家一定不要错过啦!

最新文章
1客户案例研究:专家兼容性测试,助力打造银行精品应用 通过采用WeTest兼容测试方案,该大型银行节省了近60%的设备采购和维保费用,且节省了大量测试人力。
2客户案例研究:专家安全扫描,守护金融银行小程序安全和私密性 WeTest私有化部署的定制扫描平台让金融银行客户能无成本接入扫描系统并迅速上手使用。客户能方便快捷地根据定制手册进行自助扫描,根据生成的扫描报告,详细洞察漏洞,快速识别并准确定位问题根源。
3客户案例研究:专家渗透测试,洞察电子商务小程序重大交易漏洞 通过WeTest渗透测试服务,某知名零售公司旗下的在线购物类小程序中发现了8处安全风险,我们的安全专家为客户提供了详细的漏洞报告,提供了较为清晰完整的安全加固方案。在回归测试中,中危以上风险均被解决。
4自查小程序4大安全隐患!文末免费赠送小程序安全扫描专业版! 腾讯WeTest现面向小程序开发者开放免费申请使用小程序安全扫描专业版,助您提前发现全面的安全漏洞。扫描文中问卷二维码或点击问卷链接,即可报名参与免费领取活动。
5浅谈渗透测试服务在泛互行业带来的价值 在泛互联网行业中,渗透测试服务对于保障企业的网络安全至关重要。
购买
客服
反馈