“客户都不怕,你们怕什么”——汽车软件静态分析价值的反思

汽车   2024-11-05 08:52   上海  
原话是这样的,“老板和我们说终端客户不怕死,整车厂不怕死,你们怕什么?”。

这是前段时间,和在某Tier 1的功能安全专家聊内部对功能安全的态度时,听到的一句略带无奈的话,有调侃下的偏见,但也反映了当今汽车行业的一个倾向——看得见的好才是好。

整车大跃进下,大量的开发验证工作必然要被裁剪掉,擅长“权衡之术”的我们非常自然地将诸如功能安全、网络安全、ASPICE这种追求规矩严谨但价值解释成本较高的东西给权衡掉

毕竟,“很先进”的敏捷最主要的目标就是交付价值。

今天我们聊的静态代码分析也属于这类。



1
静态分析就是MISRA吗?

面对这个问题,我们知道二者在实际工作中结合得比较紧密,但估计总隐隐约约觉得这种理解有些片面。概念的界定很重要。


首先,顾名思义,很容易理解“静态分析就是不运行程序的情况下对代码进行评估的过程”这个定义。


其次,我们再把它聚焦在汽车软件的语境里。先看下汽车电子与软件三大重要标准里的一些描述。


  • 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的静态分析主要集中在攻防相关的代码漏洞上。


狭义或按照当前项目实操来看,在汽车嵌入式软件开发中,静态代码分析多数是基于MISRA C/C++这一套规则进行自动化扫描后的评估与修改。



2
不做会怎么样?

在初步了解它是什么后,接下来,我们就经常会被问到如标题这个问题,我知道大概怎么回事了,但不做行不行?尤其是近两年不景气的光景下。


说实话,并不好回答,这就是我前面所讲的价值解释成本高。我不敢说,俄罗斯轮盘赌中的第一枪没响,第二枪会不会响,因为很多软件问题并不具备必然的因果律。


而且,当项目经理或总监拿着已经有很多打穿的枪窟窿要我填时,我很难有理由说服对方甚至自己,把血淋淋的枪窟窿先放下,去预防吧。


所以,我们不妨先稍稍搁置争议,换一种思路,如果不做静态分析,我们要做什么呢?


很显然,是运行程序的动态测试,尤其是价值体现最直观的基于功能需求的测试


但是,各位同仁们,大家扪心自问下,我们已经这样做了这么六七年了,Bug属实发现了不少,但是不是常常处于亡羊补牢的事后救火中。


还会出现,为了补枪窟窿,不停地挨枪,不断引入新问题,而且有大量的问题是偶发性的,无法复现,也永远无法得到修复,但人、钱、时间都已经并会继续花掉,原本想节省的时间和成本并没有省下来。


面对这个现状,我们想想还有什么办法没?质量左移、CICDCT、DevOps、代码级验证、白盒测试通常是可考虑的方案,而静态分析又是其中不可或缺的手段。


回到本节的问题,不做静态分析或者违反了编码规则确实不一定会给软件带来Bug,实际软件乃至实车上是否出现Bug取决于违规的性质、程度以及软件其他部分如何与违规代码交互


比如,有些违规可能只是导致代码的可读性降低或维护性变差,而并不会直接引发Bug,但一些涉及内存管理、类型转换或指针操作类违规,则很有可能导致严重的运行时错误或安全漏洞


还有个观点是,编码整体上呈现出不断犯同样的错误的现象,比如,已经存在了40年的缓存区溢出依然是当今安全漏洞的核心成员。


提高软件质量最有效的办法就是研究过去的错误,并在未来规避。


我们不能盲目地认为自己的程序员比那些制定、迭代行业标准(最佳实践)的人群更加聪明,也不能没心肝地觉得身处当今卷生卷死环境下的程序员不会犯无数程序员犯过的错误,更不能在大众传媒“颠覆性创新”这样的词汇里把一切过去的当作落后的。


实际上,软件工程几乎所有的方法论都认为:开发人员应该在早期(比如,Code Review之前)对代码进行静态检查与分析,从而将一些一般性缺陷控制在软件发布之前。


此外,在汽车软件越来越多、逐渐走向车联万物的过程中,网络安全的重要性在逐年提升(或许还需要一件大的热点事件来刺激),而黑客攻击的落脚点就是代码,代码漏洞修复的重要性不言而喻。


写到这里,或许感受到了静态分析的好的一方面,但趋利避害的大家确实都很聪明,质疑或不愿意做的背后一定有更丰富的理由,我们继续探讨。




3
为什么不喜欢做?

除了价值解释成本高这个表象之外,在汽车软件领域的实践中,还有哪些不受欢迎的点?

我们将落地过程中的问题,总结如下。

3.1 门槛高

静态分析在实践上的要求起点相对较高,除了会用工具外,不但需要安全及产品方面的知识,还需要更多的关于软件开发的知识,诸如对编译原理、数理逻辑或对应的编程语言等方面的理解。

但现实中,懂安全的多数不是从程序员走过来的,而程序员又很多是脱离产品,甚至脱离行业的。

这就会让静态分析在开展、沟通、解释、说服、理解过程中充满障碍。


3.2 误报率高

静态分析是基于逻辑推理的,再加上架构逻辑与功能定义越来越复杂,静态分析对代码上下文、运行时环境及跨模块交互行为理解的局限性更加突出。

一般我们很难仅仅通过对代码的静态分析就还原出程序的所有行为(这在计算复杂性理论里也已经有过证明)。

其中,误报率高是静态分析受人诟病的最大问题之一

3.3 解决成本高

门槛高、误报率高都会带来额外成本,这里我们聚焦在识别出潜在问题后的处理解决上。

首先,每次静态分析动辄会扫出几千几万条违反规则项,其中存在着很多误报或可偏差的内容,海量数据的一条条分析自然耗时耗力。

其次,在汽车行业花时间花钱做一件事情,需要更全盘和更纵深的考虑,需要考虑客户、考虑整车、考虑造车等等远非程序员care的东西,但这些角色会直接评判你的工作意义。

此时,结果表达(汇报)就意义非凡了,因为这些职能角色多数不会理解“循环计数器不应有实质上的浮点类型”是为何物。然而,现有的工具在结果表达上普遍不够亲民易懂。

最后,种种原因下,静态代码类问题常常没有嵌入到常规的缺陷解决流程或ALM工具中,这导致问题的跟踪、解决都比较耗体力。



4
一些优化的方法

针对以上问题,我们可以尝试以下的一些方法:


4.1 定制及优化规则集


在整个开发周期中,软件会进行多版本的迭代,所以,除了对最新行业标准的解读,更应该结合产品特点、编码习惯和经验教训,对规则进行抑制、修改和补充。比如,可以通过忽略特定文件、函数或非功能安全相关以及不变更的模块等手段来抑制误报。



4.2 调整分析敏感度平衡漏报与误报


专业的静态分析工具有很强大的自定义能力,允许客户调整分析参数,比如,敏感度阈值、分析深度等,这可一定程度上控制漏报与误报数量。


此外,一些工具还有诸如路径敏感分析、上下文敏感分析等功能,也可以帮助提升分析精度。



4.3 定制报告格式


如前所述,结果的展示非常重要。报告应清晰展示分析结果,包括问题类型、严重程度、所在模块、修复建议等,并支持导出为多种格式(如EXCEL、PDF、HTML),便于不同角色的查阅和跟踪。



4.4 集成到工具链


将静态分析工具集成到现有的开发工具链中,是实现自动化、持续质量监控的关键。通过与IDE、Jenkins、JIRA、SVN、GIT等无缝对接,并设置代码质量指标门禁,让每次代码提交都自动触发静态分析,可驱动团队及时发现并修复问题。



4.5 利用AI来解释结果


ChatGPT等AI技术的兴起,为静态分析结果的解释提供了新的可能,比如,解释静态分析发现的复杂问题,并为开发人员提供修复建议,或者对潜在车辆问题进行预测,以解释其价值。


以上都是一些不错的思路,但真实落地需要不少的专业人士一定时间的研究性质的工作,成本不会低,比如,有能力选择适合的专业静态分析工具,并可对其高级功能进行设置的人就是少数,经常需要依赖工具厂商。

所以,对工具功能的封装与友好前端的设计非常有意义,我们并不需要那么多人花费那么多学习成本在这类细分专业领域里。



5
一些关键的代码指标

无论是静态分析还是动态测试,最终的指向和落脚其实就是软件质量,而不管是从管理的可操作上,还是质量的评判上,具体、量化通常必不可少。

所以,在实操中,行业一般会抽取、总结出来一些核心的代码度量指标,然后可利用静态分析的技术或工具进行输出,以从这个侧面评价软件的质量水平。

实际上,从静态分析的视角来看,这也是其入门的尝试和进一步落地的捷径,明确可见、可量化的指标既然是管理的抓手,也就更容易得到管理上资源的支持。

本节会总结一些常用的汽车嵌入式软件的代码指标,以供参考。


5.1 每个函数的语句数


该指标是指函数内部语句的个数,是一种基础的代码复杂度度量方式。在多数语言中,我们可以使用工具自动计算语句个数。


常见语句包含以下类型:


  • 以分号(;)结尾的简单语句

  • 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。

  • 图2M=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%是一个可参考的下限。




6
全文小结

这是一篇有关汽车软件静态分析价值反思的文章

首先,描述了包括静态分析在内的一系列“好得不明显”的工作在行业里不受重视的现状。

然后,针对汽车软件领域这个特定语境下的静态分析概念、价值及弊端进行了总结与思考。

顺着弊端,提供了一些优化思路,但也应明确静态分析工具的专业化和由此而来的高门槛限制了这些思路的落地。

最后总结了11个典型的汽车行业代码指标,以让静态分析的工作更具体、更有指向性。



7
写在最后

我始终认为工具是平衡人性与规则、平衡敏捷与标准化的最好方法

对于静态代码分析这样一件在关键处颇有价值但还是有一点昂贵的事情,一个快速扫描的、兼顾精度的、容易二次开发的、界面友好的、易于上手的、报告简单易懂的,甚至嵌入AI大语言模型的工具非常有价值,这也是我们国产工具链的机会


汽车电子与软件
每天分享一篇技术文章!
 最新文章