在代码静态分析刚刚导入项目的时候,经常会碰到这样一种情况:跑完一遍扫描,列表里面一下子冒出好几百条甚至更多的问题,如果就这么从上往下一行一行地改,很容易就把时间全耗在了那些低风险的告警上,而真正会造成崩溃、越界、资源泄漏,还有安全漏洞的问题,反而被拖到了最后面,一种比较稳当的做法,是先去做一次分层的筛选,然后再按照影响的范围、缺陷的类型、代码所处的位置,还有修起来要花多少成本,来安排处理的先后顺序。
一、结果太多时先处理哪些
当Coverity Checker跑出来的结果非常多的时候,先别急着去一条一条地修改,要先把这些结果,按照风险的大小和出现的场景给拆开来看,不然的话,同一类的问题是会被不同的人,反反复复地去判断的,到了评审会上,也很难讲清楚,为什么这一批要先改。
1、应该先把那些会让程序直接崩溃的问题拿出来处理
像空指针的解引用、数组越界、除零错,还有非法的内存访问这一类的问题,是需要靠前去处理的,因为这些东西一旦被触发了,通常就会直接造成程序的异常、任务的退出,或者是控制逻辑的中断,尤其是在嵌入式、车载、工业控制这一类的软件里面,是不能让它们长期挂在问题清单里的。
2、接着去处理资源释放和生命周期那一类的问题
内存泄漏、文件的句柄没有关掉、锁没有释放、同一个东西被重复释放了等等这些问题,也是要优先拿出来看的,在短时间内,它们不一定会马上就暴露出来,可是跑的时间一长,就容易造成资源被耗尽、死锁,或者是系统性能慢慢地往下降,对于那些一直常驻在内存里的进程、周期性地在跑的任务,还有通信的服务,这类问题需要被单独地筛出来。
3、把跟安全有关的那些Checker结果也提前去处理
那些涉及到命令注入、缓冲区写入、敏感数据没有处理好、输入没有做校验,还有用了危险函数的检查结果,都是需要提前去评估一下的,就算当前的模块并没有对外的接口暴露出来,也要去看一看,这些数据是不是从文件、网络、诊断命令,或者是用户输入那边过来的,不能简简单单地就把它判成是一个低风险的问题。
4、先去处理那些出现在主干和交付分支里的问题
同样是一条告警,如果它出现的位置是在主干、量产分支,或者是客户交付分支里面,那就比那些藏在实验代码、样例代码,或者是已经废弃不用的目录里的告警,更值得被先拿出来处理,可以按照代码的路径先过滤一遍,把那些核心的业务模块、公共的库、底层的驱动,还有通信的模块,给排到前面去。
二、优先级应该怎么划分
对Coverity Checker的优先级进行划分,不能只是盯着工具自己给出来的那个等级看,也不能光看数量的多少,工具给出来的等级,只是一个起点,项目上还需要结合代码的用途、被触发的条件、历史上发生过的缺陷,还有交付的时间节点,去做出第二轮的判断。
1、按照缺陷带来的后果来划分
那些会造成程序崩溃、数据被破坏、控制流程异常,还有安全风险的问题,把它们放在高的优先级上;那些会造成偶然性的错误、资源慢慢地被累积起来、异常分支处理得不够完整的问题,放在中等偏上的优先级上;而只影响到代码的规范、可读性,或者是那种极小概率下才能碰到的边界问题,就可以安排在后面的批次里去处理。
2、按照被触发的概率来划分
处在那些被频繁调用的路径上、初始化的流程里、周期的任务中、通信的收发环节、文件解析,还有外部输入的处理逻辑里的问题,是要把优先级给提上去的,反过来,只有在测试工具、调试开关,或者是很老旧的兼容逻辑里面才会被触发的问题,可以先记录下原因,再安排后面去处理。
3、按照模块的重要性来划分
核心的算法、权限的控制、存储的读写、协议的解析、异常的处理、启动的加载,这几类模块的告警,是要重点拿出来看的,公共的函数库也要提前去处理,因为在公共函数里的一个缺陷,很有可能会牵连到很多个调用它的地方,后面再去排查起来,会比较的麻烦。
4、按照修复起来要花的成本来划分
有些问题,只需要补上一个空指针的判断、增加一个返回值的检查、或者是调整一下资源释放的顺序,这就可以很快地被关掉;而有些问题,就需要去改动接口、拆开原来的逻辑、重构对象的生命周期,那就要走变更评审的流程了,那种短平快的问题,可以先清掉一批,但是,不能为了图省事,就绕开了那些高风险的复杂问题。
三、问题怎么形成闭环
Coverity Checker扫出来的问题被处理完了以后,还要把当初的判断和验证的记录给留下来,不然的话,下一次扫描,同样的问题又冒了出来,团队还是得从头再判断一遍,那这个工具的价值,就被白白地消耗掉了。
1、去建立一套统一的判定口径
团队内部需要先约定好,哪些Checker是必须要处理掉的,哪些是允许结合具体的场景,把它标记成误报的,又有哪些是需要架构或者安全的负责人来确认的,不要让每一个开发人员,都按照自己的习惯去处理,要不然,同样一类问题,最后会得出来不一样的结论。
2、把误报的原因给写具体了
如果判定下来是一个误报,那就不要只在上面留下“误报”两个字就完了,要去说明一下,为什么这个地方是不会被触发的,比如,那个指针在进到这个函数之前,就已经被检查过了;数组的长度,是由它的上一层来限制住的;资源的释放,是交给了一个统一的框架去管的,只有把这些理由都给写清楚了,后面再来复审的时候,才会有依据。
3、代码改完以后要重新扫描确认一下
代码被改掉了之后,是需要重新再去跑一遍扫描的,或者至少也要对相关的模块,做一次增量的扫描,关闭一个问题,不能仅仅靠着代码评审时候的口头确认,扫描出来的结果、代码提交的记录、评审的意见,还有测试的结果,这几样东西之间,是要能够对应得起来的。
4、把那些反复出现的高频问题,沉淀成固定的规则
如果有某一类问题,总是在反反复复地出现,比如函数的返回值老是不去检查、空指针的保护总是缺着、资源释放的路径前后不一致,那就很有必要把它写进编码的规范,或者是评审的清单里面去,后面在代码评审的那个阶段,就提前把它给拦住,这比等到扫描全都跑完了,再集中起来返工,要省力得多。
总结
当Coverity Checker扫出来的结果太多,先处理哪些,比较建议的顺序是先去看崩溃类的、资源类的、安全类的,还有落在交付分支里的那些问题;而Coverity Checker的优先级又要怎么去划分,这需要结合着缺陷带来的后果、被触发的概率、模块本身的重要性,还有修复起来要花多少成本,综合起来去判断,处理出来的结果,不能只是停在列表的状态发生了变化,后面还要有修改的记录、对误报的说明、复扫的结果,还有团队规则的沉淀,这么做下来,静态分析才不会变成一次性的告警清理活动。
