面对这个问题,我们知道二者在实际工作中结合得比较紧密,但估计总隐隐约约觉得这种理解有些片面。概念的界定很重要。
首先,顾名思义,很容易理解“静态分析就是不运行程序的情况下对代码进行评估的过程”这个定义。
其次,我们再把它聚焦在汽车软件的语境里。先看下汽车电子与软件三大重要标准里的一些描述。
ASPICE 4.1:
软件单元验证措施的例子包括静态分析、代码评审和单元测试。其中,静态分析可以根据MISRA规则集和其他编码标准进行。
ISO 26262:
软件单元验证方法包含走查、结对编程、检查、半形式验证、形式验证、控制流分析、数据流分析、静态代码分析、基于抽象解释的静态分析、基于需求的测试、接口测试、故障注入测试、资源使用评估、在模型和代码之间背靠背对比测试(如果适用)。
静态分析是一个集合术语,包括诸如搜索源代码文本或模型以查找与已知故障匹配的模式或符合建模或编码指南的分析。
基于抽象解释的静态分析是扩展静态分析的集合术语,包括诸如通过添加语义信息来扩展编译器分析树之类的分析,这些语义信息可以检查是否违反了定义的规则(例如,数据类型问题、未初始化的变量);包括控制流图生成和数据流分析(例如,捕获与竞争条件和死锁、指针误用相关的故障)、或甚至元编译及抽象代码/模型解释。
ISO/SAE 21434
对于网络安全所适用的设计、建模或编程语言,若语言本身未涵盖相关标准,则应通过设计、建模和编码指南,或由开发环境来补充这些标准。
例如,在“C”编程语言中进行安全编码时,应使用MISRA C或CERT C。
尽管国外标准翻译成中文后,语法排布上总有些拗口,但总结以上,我们还是可以得出汽车软件语境里共识的三个信息:
静态分析是软件单元验证的一种或一部分,处于汽车软件测试的最下一层级。
静态分析是基于包括MISRA C但不限于此的内置规则集(比如,MISRA C++/AC、AUTOSAR C++、CERT C/C++、HIS)进行代码成分分析与质量判断的过程,会使用到控制流分析、数据流分析、抽象语法等技术手段。
静态分析在对安全性与可靠性要求较高、以C/C++为基础的汽车嵌入式软件中,尤其是功能安全与网络安全机制较多的产品或代码部分,更具价值。而通常认为,Java语言相比较C/C++,没有那么多未定义或未明确的行为,不存在类似MISRA这类普遍接受的安全类标准,对Java的静态分析主要集中在攻防相关的代码漏洞上。
在初步了解它是什么后,接下来,我们就经常会被问到如标题这个问题,我知道大概怎么回事了,但不做行不行?尤其是近两年不景气的光景下。
说实话,并不好回答,这就是我前面所讲的价值解释成本高。我不敢说,俄罗斯轮盘赌中的第一枪没响,第二枪会不会响,因为很多软件问题并不具备必然的因果律。
而且,当项目经理或总监拿着已经有很多打穿的枪窟窿要我填时,我很难有理由说服对方甚至自己,把血淋淋的枪窟窿先放下,去预防吧。
所以,我们不妨先稍稍搁置争议,换一种思路,如果不做静态分析,我们要做什么呢?
很显然,是运行程序的动态测试,尤其是价值体现最直观的基于功能需求的测试。
但是,各位同仁们,大家扪心自问下,我们已经这样做了这么六七年了,Bug属实发现了不少,但是不是常常处于亡羊补牢的事后救火中。
还会出现,为了补枪窟窿,不停地挨枪,不断引入新问题,而且有大量的问题是偶发性的,无法复现,也永远无法得到修复,但人、钱、时间都已经并会继续花掉,原本想节省的时间和成本并没有省下来。
面对这个现状,我们想想还有什么办法没?质量左移、CICDCT、DevOps、代码级验证、白盒测试通常是可考虑的方案,而静态分析又是其中不可或缺的手段。
回到本节的问题,不做静态分析或者违反了编码规则确实不一定会给软件带来Bug,实际软件乃至实车上是否出现Bug取决于违规的性质、程度以及软件其他部分如何与违规代码交互。
比如,有些违规可能只是导致代码的可读性降低或维护性变差,而并不会直接引发Bug,但一些涉及内存管理、类型转换或指针操作类违规,则很有可能导致严重的运行时错误或安全漏洞。
还有个观点是,编码整体上呈现出不断犯同样的错误的现象,比如,已经存在了40年的缓存区溢出依然是当今安全漏洞的核心成员。
提高软件质量最有效的办法就是研究过去的错误,并在未来规避。
我们不能盲目地认为自己的程序员比那些制定、迭代行业标准(最佳实践)的人群更加聪明,也不能没心肝地觉得身处当今卷生卷死环境下的程序员不会犯无数程序员犯过的错误,更不能在大众传媒“颠覆性创新”这样的词汇里把一切过去的当作落后的。
实际上,软件工程几乎所有的方法论都认为:开发人员应该在早期(比如,Code Review之前)对代码进行静态检查与分析,从而将一些一般性缺陷控制在软件发布之前。
此外,在汽车软件越来越多、逐渐走向车联万物的过程中,网络安全的重要性在逐年提升(或许还需要一件大的热点事件来刺激),而黑客攻击的落脚点就是代码,代码漏洞修复的重要性不言而喻。
写到这里,或许感受到了静态分析的好的一方面,但趋利避害的大家确实都很聪明,质疑或不愿意做的背后一定有更丰富的理由,我们继续探讨。
4.1 定制及优化规则集
在整个开发周期中,软件会进行多版本的迭代,所以,除了对最新行业标准的解读,更应该结合产品特点、编码习惯和经验教训,对规则进行抑制、修改和补充。比如,可以通过忽略特定文件、函数或非功能安全相关以及不变更的模块等手段来抑制误报。
4.2 调整分析敏感度平衡漏报与误报
专业的静态分析工具有很强大的自定义能力,允许客户调整分析参数,比如,敏感度阈值、分析深度等,这可一定程度上控制漏报与误报数量。
此外,一些工具还有诸如路径敏感分析、上下文敏感分析等功能,也可以帮助提升分析精度。
4.3 定制报告格式
如前所述,结果的展示非常重要。报告应清晰展示分析结果,包括问题类型、严重程度、所在模块、修复建议等,并支持导出为多种格式(如EXCEL、PDF、HTML),便于不同角色的查阅和跟踪。
4.4 集成到工具链
将静态分析工具集成到现有的开发工具链中,是实现自动化、持续质量监控的关键。通过与IDE、Jenkins、JIRA、SVN、GIT等无缝对接,并设置代码质量指标门禁,让每次代码提交都自动触发静态分析,可驱动团队及时发现并修复问题。
4.5 利用AI来解释结果
ChatGPT等AI技术的兴起,为静态分析结果的解释提供了新的可能,比如,解释静态分析发现的复杂问题,并为开发人员提供修复建议,或者对潜在车辆问题进行预测,以解释其价值。
该指标是指函数内部语句的个数,是一种基础的代码复杂度度量方式。在多数语言中,我们可以使用工具自动计算语句个数。
常见语句包含以下类型:
以分号(;)结尾的简单语句
if语句
for语句
while语句
do语句
switch语句
break语句
continue语句
return语句
goto语句
语句个数应尽量维持在10~20,最多不要超过50。
5.2 return语句的数量
为了提高代码的可读性,我们最好遵循“单一出口原则”,也就是尽量保证一个函数只有一个出口点(函数结束执行的地方)。
出口点通常是return语句,所以我们也建议尽量减少其数量,比如,一个函数的return维持在1~2个之内。
5.3 代码行长度
写文章要短句,是为了便于阅读。代码也是一样,太长的代码行会明显增加阅读代码的难度,很现实的问题是,需要开发人员左右滚动屏幕。
在保证合理的逻辑、换行和缩进的前提下,要尽可能将长代码拆分。通常,低于160个字符的代码行可以认为是一个合理目标。
5.4 圈复杂度
圈复杂度是指通过源代码线性独立路径的个数,也是用来衡量代码复杂度。
如何计算呢?我们可以通过以下3个代码控制流图来看。
一种计算公式为,圈复杂度M=控制流图边数E-节点N+2
故,
图1:M=1-2+2=1,即无判定节点,圈复杂度为1。
图2:M=4-4+2=2,即一个判定节点,圈复杂度为2。
图3:M=7-6+2=3,即两个判定节点,圈复杂度为3。
除了评价本身代码判定逻辑的复杂性之外,圈复杂度还能够用来确定最少需要多少个测试用例来满足分支和路径的覆盖度。
一般经验是,圈复杂度应小于10,以达到较好的可测性。
5.5 非循环路径数
这个指标也被称为NPATH,是指通过软件所有可能路径的数量。其中,循环中的循环(for, while, do-while)只访问一次。
因此,NPATH也给出了达到路径覆盖所需的测试用例的最大数量。而圈复杂度给出了测试用例的最小数量。
如下图,对应的圈复杂度和NPATH分别为3和4。NPATH建议限制在80个以内。
5.6 每个函数的嵌套级别
嵌套级别用来描述函数之间调用的深度层次。
当引入控制结构(if, while…)时,就会发生嵌套,每将控制结构放置在其他控制结构内部一次,嵌套级别就会增加一次。
以下为一个嵌套级别为2的代码段示意。
if (a < K) {
if (b > L) {
function);
}
}
嵌套级别建议不超过4。
5.7 调用图递归
调用图是软件工程中用于表示函数调用关系的有向图,它显示了哪个函数调用了哪个函数。
调用图内部的递归是一个函数直接或间接(通过至少一个其他函数)再次调用自身的模式。
递归是一种很好的编程技巧,但在嵌入式中有一些缺点。
要想停止递归时,必须有一个结束条件,否则,递归将导致应用程序崩溃,但是,无论是直接递归还是间接递归,确定结束条件并不容易。
此外,由于递归算法需要更多的函数调用和堆栈操作,其使用会造成性能阻塞、可读性差或堆栈溢出等问题。
一般不建议使用递归。
5.8 不同函数的调用次数
更多的函数调用必然带来更大的复杂性,整体最好不超过7次。
5.9 参数数量
参数的数量是函数复杂性和接口复杂性的另一个指标。存在的参数越多,就越容易在调用函数时出错,比如,参数顺序错误。
如果函数参数超过了5个,可以试着把函数分成使用较少参数的逻辑部分。
5.10 goto语句
goto语句可以使程序直接跳转到同一函数中的预定义位置。
goto是一个很有争议的语句。在处理错误或跳出多层循环时,有很直接的效果,但非逻辑性的跳转会让代码很难理解、出了错误也很难追踪。
所以,通常强烈建议不要使用goto语句。
5.11 注释密度
除了从语句结构上降低外,代码复杂度还有一种应对方式是代码注释。
函数功能的文本化描述就是注释,这显然有助于理解代码。特别地,代码已经长时间没有被修改,或者代码必须由原始编写人分析或修改时,这些注释更加有用。
一种算法是,注释密度是指第一个语句之后找到的注释数量与语句数量之间的比率,20%是一个可参考的下限。