基于编译器的静态代码分析与软件开发效率、质量和性能
本文节选自《基础软件之路:企业级实践及开源之路》一书,该书集结了中国几乎所有主流基础软件企业的实践案例,由 28 位知名专家共同编写,系统剖析了基础软件发展趋势、四大基础软件(数据库、操作系统、编程语言与中间件)的领域难题与行业实践以及开源战略、生态建设与人才培养。
1 作者介绍
李隆: 目前担任鉴释科技的首席科学家,专注于代码验证基 础架构的研发。于 2008 年在中国科学技术大学获得计算机软件和理论博士学位,专门研究基于编程语言的理论、技术在构建高效、可靠软件方面的应用,并发表了数篇期刊和会议论文。毕业后,加入了三星电子,在先行技术小组从事统计机器翻译的研发工作。于 2010 年加入 HP 编译器团队,从事 HP Non-Stop 服务器 编译器后端及工具链的研发工作。
注:本文整理自李隆博士在 DIVE 全球基础软件创新大会 (2022)上的演讲,由李冬梅整理
随着软件规模不断增大,架构日益复杂,在软件开发过程 中兼顾开发效率、软件质量和软件性能越来越困难。为了追求更 高的开发效率和更短的交付周期,软件质量和性能往往成为被忽 视甚至牺牲的对象,给软件开发带来长期损害。静态分析是指 在不实际运行程序的前提下,利用编译器技术中的词法 / 语法分 析、控制流 / 数据流 / 别名 / 上下文分析建立代码的静态视图,在 此基础上应用各种检查逻辑和规则来确定程序中影响软件质量和性能的问题代码。通过静态代码分析工具与 DevOps 流程紧密结合、 CI(Continuous Integration,持续集成)/CD(Continuous Delivery,持续交付)集成和扫描差分报告,在编码过程中实施编码规范和惯用法检查,结合预定义和自定义规则的语义和业务逻 辑检查,实现更高的开发效率、更好的软件质量和软件性能。
下面主要从四部分展开,分别是静态分析工具在当前软件开发流程中的应用、编译相关技术在静态分析工具中的应用、编译 相关技术在提升软件质量和性能上的更多应用以及未来展望。
一、 静态分析工具在当前软件开发流程中的应用
静态分析工具(SAST)在不运行程序的情况下进行源代码、代码中间表示,甚至二进制代码上的分析。这种方式用于发现程序中的潜在漏洞并及早修改,从而使发布的程序更安全、更可靠。静态分析也可以用于程序优化,提升程序性能。后者通常由编译器自动完成,无须开发人员参与。
一个完整的软件开发生命周期遵循需求分析→详细设计→代码实现→测试→客户端部署→维护的流程,如图 4-2-1 所示。程序在正式交付之后,根据客户的实际需求还会做相关的调整,并 继续迭代。静态分析不仅可以在代码实现、测试环节,甚至在程序部署环节都能发挥一定作用。
图 4-2-1 完整的软件开发生命周期
我们为什么要强调在软件开发生命周期中应用静态分析?根据IBM的统计数据,开发阶段的修复缺陷的相对成本最低,在测试和产品发布后修复缺陷的相对成本将会大幅增长,如图 4-2-2 所示。
图 4-2-2 修复缺陷的相对成本
注意:修复在开发过程中发现的错误会致使成本提高6.5%,而修复在测试阶段发现的错误则致使成本提高15%,产品发布后 修复错误会致使成本提高100%。
另一份报告中则指出,缺陷修复成本是随着周期的推移而逐渐增高的,代码缺陷数量大多出现在开发阶段。但是实际的缺陷修复大部分都在较为靠后、成本相对较高的阶段。通过静态代码分析工具,我们可以在较早期,也就是开发阶段找到缺陷。在此时修复缺陷不仅可以提高程序的安全性和可靠性,而且可以极大地节约成本。这也就是为什么目前静态分析愈发重要,也愈发被各个开发厂商所强调。
二、 编译相关技术在静态分析工具中的应用
开发人员在代码编写阶段可能更熟悉代码的自动补全。这是一个几乎所有IDE上都有的简单功能,能够极大地提升开发人员的编写效率,如图 4-2-3 所示。
图 4-2-3 代码自动补全
另一种开发人员熟悉的功能则是编码规范的检测。像是 MISRA 之类的规则集中虽然包含很多编码规范相关内容,但是检测其实也可以放在 CI/CD 的流程中。在软件开发生命周期流程中, CI/CD 可以进行各种漏洞和规则的检测,如检测常见的空指针引用或使用未初始化变量等漏洞,以及检测 CERT 、OWASP、 CWE 、CVE 等规则集下的合规性。
此外,软件成分分析也可以在CI/CD 中进行。软件成分分析工具主要用于检测所提交代码中许可证的合规性。目前虽然开源软件资源丰富,但如果不留心授权问题,就很可能导致风险,甚至面临法律问题。另外,软件成分分析还可以针对第三方软件中的已知漏洞给予提示,并提供更完整的信息作为参考,这对是否使用该第三方软件具有决定作用。很多开源软件库虽然强大,但我们真正用到的只是其中一小部分,软件成分分析工具能帮助我们做出更好的判断和选择。在部署阶段,静态分析工具还可以对二进制代码、字节码进行检测。它们是代码真正部署到客户端前,静态分析工具可以做的最后尝试,这类应用的需求虽然存在,但并不广泛。
这些静态分析工具中用到了哪些技术呢?
1. 静态编辑工具背后的支撑技术
代码自动补全功能主要用到了计算机编译中的词法、语法分析。编码规范不仅囊括了语法、词法分析,还需要在抽象语法树上通过模式匹配,查找并就代码中与规范相违背的部分给出警告。这些在编译中都应算作前端的技术内容。
编码规范的检测大部分情况下只能覆盖较为简单的前端规则,要检验稍微复杂的规则或漏洞则不仅要处理抽象语法树,还需要对代码中间表示,甚至是二进制代码进行模式匹配。
然而,语法、词法上的模式匹配有其局限性。若一款静态分析工具要处理更深层次的问题,则还应在代码中间表示上进行语义分析,并利用编译中的“符号执行”技术。可以说,在语法层面和语义层面进行双重模式匹配,基本可以覆盖一个程序在静态分析阶段所能处理的所有问题场景。
对于软件成分分析来说,目前绝大多数工具都只是将代码 以文本的形式加以处理,基于文本特征值进行相似度的计算。因此,这一类工具的准确度和有效性还不是十分理想。
2. 静态分析工具的衡量指标
我们要如何衡量一款静态分析工具的好坏呢?对于用户来说,最直观的感受无非是准确率和资源消耗。
(1)准确率
影响准确率的因素可以分为误报率和漏报率。误报是指分析工具错误地将正确代码报错的情况,漏报则是指分析工具没能找 出代码中的问题。用户在工具的使用过程中通常对漏报无感,因为他并不知道代码中还有什么问题没有找出来;大多数用户更在意的是分析工具的误报率。当然,现在也有很多测试集可以体现分析工具的漏报情况,但这些测试集所覆盖的范围也都非常有限。若要提升准确率,静态分析工具应能处理以下技术内容:
1)上下文敏感。如果一款工具可以完整、正确地构建函数之间的调用关系图,那么它就可以更好地处理带有上下文、跨函数、跨文件的分析。
2)流敏感。工具要能够处理好控制流图以及静态单赋值情况。
3)跨维度分析。工具要能够处理跨函数、跨文件,甚至是跨语言的分析。
这三项技术内容决定了一款分析工具在准确度指标上的表现是否优秀。
(2)资源消耗
用户平时感触最深的就是响应时间和成本。响应时间是指用户希望分析工具可以快速检测出问题并返回结果,而不必期间先去处理其他事务再回来看结果。成本则是对用户财力的考量,如果用户需要采购大量服务器以支撑分析工具,那么成本将会非常高昂。
3. 用户自定义规则
目前也有静态分析工具的开发厂商提供二次开发功能。这是因为不同的用户,其需求也不同。静态分析工具的开发厂商不可能做到全面覆盖所有用户的所有需求。针对这种情况,有两种解决方案。一是工具开发厂商根据用户需求进行开发,但用户需要承担高昂的开发成本。二是工具开发厂商预留二次开发功能,让用户根据自身需求对分析工具进行二次开发。针对二次开发的需求,静态分析工具需要具备基于符号执行的语义分析的能力,通过动态链接的形式,允许用户自行开发的规则按需链接到工具的扫描引擎中。
4. 第三方库的分析
大多数用户在使用第三方库时,对库中API 的设计难以有完美的理解,这也就意味着在库的实际使用过程中会产生分歧。常见的使用第三方库出现的问题,大多是由于用户对库的调用与其真实的语义之间存在差异。因此,用户希望工具的开发厂商能够 给出类似C库的标注以减少歧义,或提供类似二次开发的功能,以便能够自行完成对第三方库的分析标注,从而真正将链接的第三方代码包含到分析中来。
三、 编译相关技术在提升软件质量和性能上的更多应用
编译技术还可以提升软件的性能,在编译中又称编译优化。其使用场景包含并不限于以下方面:
1)公共子表达式删除:通过减少程序执行的指令数目来提高程序性能。
2)函数内联(inline):通过减少调用消耗,来提升性能。
3)死代码删除:主要针对代码规模的优化。
比如,在 MISRA 规则集中有这样一条规定,程序中不应存在死代码。比如下面这个例子—foo()。
void foo() {
…
if (1) {
…
} else {
…
}
}
因为if 的条件是永真的,所以永远都不会走else的分支,在编译优化中,编译器可能会将全部else 分支,甚至if 的判断语句 全部删除。但MISRA规则集只会提示我们有一段代码不会被执行。那么大家可能会问,既然编译优化已经能够将死代码删除,为什么我们还需要 MISRA 这样的规则集去进行检测呢?在实践中,死代码的出现可能是历史原因造成的,如长期的开发、版本 迭代后,会遗留一些不明所以的东西在代码中;死代码的出现也 可能是由于写代码的人当时出现了思维误差,或是手误,else 分支的执行需求确实存在,但因为思路或想法不够完整,所以条件 语句中的表达式变成了永真值。这样的报错提示也可以促使软件开发者仔细思考这个判断条件中到底应该写什么,到底需要处理什么样的场景。简单删除死代码,虽然对程序执行没有影响,但却可能导致功能上的缺失。
1. 编译优化对静态分析工具的影响
编译优化不仅与静态代码中的规则检测和优化有关,而且会影响静态分析工具。对基于编译优化的静态分析工具而言,可以被编译优化掉的 代码错误是不会被工具检测到的。以下面一段代码为例,虽然变量x不会被使用,但在第三行有一个除以零的错误。在真正运行时,因为变量 x 已经被优化掉了,所以绝大部分编译器都不会运行到除以零的这行,也就不会执行 1 除以 0 的操作。但对静态分析工具而言,如果没有考虑到优化的内容,那么这里还是会报出除以零的错,当然这里的代码也是一段死代码。
int foo(){
int x;
x = 1/0;
return 0;
}
函数内联对静态分析工具也有极大的影响。如前文所讲,一款工具的准确度在很大程度上取决于其能否正确处理上下文信息,而函数内联是能够帮助分析工具最完整地获得上下文信息的 手段。函数内联也会造成函数规模变大,导致分析工具所要占用的内存和时间直线上升。编译优化所做内联会在代码大小、代码效率和执行速度间寻找一个平衡点,但这也就造成了静态分析工具的一个能力缺陷。对依赖函数内联的静态分析工具而言,函数内联的层数就是其上下文分析的准确度的极限,对更深层次调用 的分析将不再准确,误报率、漏报率都会明显上升。
2. 更进一步地提升性能
为进一步提升程序性能,编译器还可以基于反馈进行优化。通过部署到实际运行环境中代码内的插桩进行数据收集,编译器以这些数据为参考,再重新进行优化。较为常见的应用有分支预测,只有在实际环境中运行了足够长的时间后,我们才能准确知道哪些分支被执行的次数较少,从而让编译器在做程序优化时,将执行次数较少的分支作为一个跳转的目标,以实现在下次部署 时达到更好的性能。在DAST(动态代码分析工具,可以测试Web、移动和 API 应用程序,通过模拟攻击来发现漏洞/ 安全漏洞,结合自动扫描仪或手动渗透测试实践来实时测试应用程序)或 IAST(交互式自动代码分析工具,可在应用和API 中自动识别和诊断软件漏洞)中,插桩也可协助提升软件质量。举例来说,如果程序没有进行空指针或数组越界的检查,那么编译器可以在程序进行指针的解引用、数组下标访问等危险操作前直接插入检查和纠错的代码, 同时收集部署运行时的数据并反馈给开发者,让开发者判断是否需要进行相应的修改。发生指针解引用为空或数组访问越界的情况的原因,除了开发者在编写代码时有疏漏外,还有可能是开发者在对数据逻辑进行假设时出现了问题。这样的信息对开发者而言也是有用的。
3. 静态分析工具现状
我们来看下静态分析工具的应用现状。比如:在代码编写时,开发者会用到集成在 IDE 中的代码补全、语法高亮等功能;软件开发商也会广泛使用 CI/CD 流程中的漏洞检测、规范规则检测。 但后者的检测结果往往得不到开发者的重视。原因有二:一是目前静态分析工具的误报率仍然偏高;二是历史遗留问题,开发者在接手一段代码后通常不会有兴趣去研究或修改前人编写的代码。这并不稀奇,我们都知道看别人写的代码很不容易。
因此,我们现在更强调分析工具的增量扫描结果。以谷歌发布的结果中可以看出,通过强调单次代码提交在原有基准线上新引入缺陷和漏洞的增量式报告,开发者会更有动力、更积极地进行代码修复,因为开发者仍在关注当前问题,所以修复的速度也很快。总的来说,在开发过程中强调增量扫描,是软件开发过程中更好的选择。
四、 未来展望
这些年来,随着AI 技术的愈发火热,不少人尝试在静态分析工具中加入AI 相关的技术以分析缺陷和漏洞。但目前仍未有强有力的数据可以说服厂商或开发人员使用AI 技术替代当前的静态分析工具。
另外,目前大多数静态分析工具所依赖的仍然是编译中的前端技术,这也就意味着还有很多中间优化技术以及后端深层次技术没能很好地引入静态分析工具中来。利用这些已有的、成熟的 编译技术进一步完善静态分析工具,也是一个非常可行的发展方向。但许多大厂由于所处的市场地位,在对已有成熟工具进行大规模修改时难免遇上很大困难。
我个人认为,未来软件成分分析之类的工具可以考虑将程序特征加入分析的特征值中,而不是现在这样单纯基于纯文本做分析。
目前软件成分分析工具所面临的一个大问题就是,如果我们 将程序中的关键字加以替换,那么基于纯文本的分析工具所计算出的特征值将会有很大差异。这也就是说,如果我们简单地进行足够次数的全局替换,分析工具将无法识别程序中抄来的代码和有版权约束的开源代码之间有什么差别。但如果开源方认真查看这些全局替换的代码,会很容易地发现抄袭。
图书推荐
随着云计算和生成式 AI 的逐渐发展,基础软件的技术栈也在发生变化,市场现存的基础软件领域的图书相对较少,且多数最近两年没有更新。但是,基础软件领域已经发生了巨大变化,我们现在所讲的基础软件是以云、AI 为底座的基础软件,这些新的变化都可以在**《基础软件之路:企业级实践及开源之路》**这本书里找到答案。