代码在计算机上运行,但用途并不限于此,同样是用来阅读和理解的。不易理解的代码不能轻易地维护或改进,代码库的复杂性与其缺陷率之间存在着相关性。理解大型的代码库是困难的,因此需要各种工具和技术来协助。
静态代码分析(简称静态分析)工具,是由程序或算法组成的,从一个程序的源代码中提取事实,而不需要执行有问题的程序,通常作为日常软件开发过程中的一个特定阶段。通过使用工具进行静态分析,有机会使用分析中产生的事实来进一步理解、评估和修改相关的代码。可以从源码中提取的数据目标可以分为很多类,例如,安全漏洞的分析会提取程序中的函数和库信息,而代码布局的分析则会关注语法结构的位置。常见的目标还包括无用代码检测,是否使用了不安全的 api,识别和监视可能被恶意数据污染的值, 竞争条件检测,确保对数组或内存位置的访问在预期的范围之内,等等。
静态分析和动态分析常常同时使用。动态分析从程序的运行时提取事实数据,检查并验证程序的正确性。静态分析的一个好处是,通常可以操作程序中所有可能的执行分支,而动态分析只能访问当前正在执行的代码路径。然而,动态分析有关于数据在运行程序内存中的布局和位置的具体信息,而静态分析则必须猜测给定的语言、编译器、操作系统和计算机体系结构将如何表示特定的数据。另外,二者有一定程度的重叠,例如动态分析和静态分析都可以检测 c 语言中未初始化变量的使用。
静态分析在软件工程中的使用非常普遍,例如 scan-build 是 LLVM 项目提供的一种分析工具,目标是 C、 Objective-C、 C 和 Swift; Python 的 black或 Go 的 gofmt 是代码格式化的工具; Rust-analyzer是Rust 的模块化编译器。
静态分析也是其他几个领域的核心。例如,编译器设计,大多数编译器在生成代码之前和之后运行许多单独的静态分析。实际上,可以将编译器大体上看作一个静态分析工具,这个工具生成的数据包括可执行程序以及调试信息。然而,静态分析一般指的是可以与编译器或构建系统一起使用的外部工具。
静态分析有着基本的限制,其局限性意味着静态分析常常局限于逼近程序的真实行为。尽管如此,一个近似值在实践中是非常有用的。
静态分析的过去
静态分析工具是一个程序,通常独立于程序执行机制(如解释器或编译器) ,在独立工具出现之前,静态分析是作为现有程序的一部分出现的。
类型推断算法的第一次发展出现在1958年,这些算法既可以检查程序中声明的值类型,也可以在没有明确声明的情况下推断值的类型。接下来,大量关于分析的研究聚焦于编译器的优化。今天的静态分析工具是在20世纪70年代发展起来的。最早的分析工具是Lint,写于1978年,1979年伴随着 Unix 而发布,用于检查 c 程序的错误。那时的 C编译器执行的正确性检查远远低于现在的编译器,lint 引入了一些流行的分析方式,例如关于可疑类型转换的告警、不可移植的结构、未使用或未初始化的变量等等,这些告警现在通常已经是 c 编译器本身的一部分了。Lint 的影响力巨大,它的名字已经成为许多编程语言静态分析工具的一部分。
随着时间的推移,静态分析已经成为了许多软件开发方法的基本部分。
静态分析的发展
因为语言之间在语法和语义上有很大的差异,大多数静态分析工具都是针对单一语言的。语言的特性对相关分析工具的行为和需求都有着巨大的影响,例如,面向支持字符串运行时代码求值(eval ())的语言如 JavaScript 和 Python,必须要考虑到这些特性。在没有 eval ()的语言中,就不需要考虑这一点。另外,针对高级语言的分析不需要考虑低级内存访问的风险。
如今,有许多方法可以在代码库中引入静态分析。许多版本控制软件服务,无论是免费的还是商业的,都提供了一个集成的静态分析工具平台,当新代码被推送到仓库时,就可以按需执行分析。静态分析工具还与持续集成服务协助,如果分析工具报告有意外结果,则构建过程就会失败。使用现有的分析框架扩展,我们可以编写自己的静态分析工具,例如,scan-build 提供了一个 API,使最终应用能够挂接到 LLVM 的内部进程,并利用 LLVM 的工具链来遍历和分析程序的语法树。
静态分析及其相关语言之间的影响是双向的,静态分析的发展推动了编译器和相关工具的发展,例如Swift语音的出现。Objective-C 内存管理最初是手动引用计数,与 C 编译器相关联的静态分析可以检查是否正确保留和释放内容。ARC的引入,使Objective-C 成为了具有正式内存管理策略的语言,为Swift 的诞生铺平了道路。类似地,类型推理分析促进了 ML 语言家族的发展,并将类型推理引入到缺乏类型推理的语言中,例如C 。
静态分析的应用
静态分析在应用中有几种方式,最直接的分析方法是在其本地的机器上运行分析。许多流行的文本编辑器和 IDE自动集成了静态分析工具,在我们开发软件时直接向提供分析反馈。静态分析一般集成到开发和构建过程中。然而,许多静态分析是看不见的,例如一个编译器本身就是一个静态分析,数十个单独的静态分析联接在一起,生成一个可由计算机执行的程序。甚至,文本编辑器也执行自己的分析,几乎所有的编辑器都使用了语法高亮,这也是一种静态分析,可以产生关于程序中使用的标识符和关键字的语义信息。此外,在开发过程中使用的大部分软件都是静态分析的,包括操作系统乃至 CPU 的微代码。
并非所有的分析在实践中都是可行的。代码库越大,解析和遍历所需的时间就越长。因此,静态分析和被分析的代码库之间是一种军备竞赛。随着代码库变得越来越大,程序员需要更加复杂且高效的分析。
采用静态分析工具的一个障碍是要求人们改变自己的行为,以解释发现的问题和出现的告警。自从 lint 出现以来,工程师一直在努力消除与误报结果相关的告警,通常是在代码中插入”魔法注释”。然而,手工插入这些指令很乏味,工程师通常会选择不使用会产生太多误报的静态分析工具。工程师通过仔细配置给定工具来避免假阳性,而假阴性更难发现,一般通过同时使用多个静态分析工具来降低风险。
静态分析的未来
现代的静态分析工具为代码库提供了强大的洞察力。例如,Linux 内核团队开发了Coccinelle,用于搜索、分析和重写 C的源代码,由于 Linux 内核包含2700多万行代码,因此静态分析工具对于发现 bug 以及对其许多库和模块进行自动更改都至关重要。针对 C语言家族的另一个工具是 Clang scan-build,它提供了许多有用的分析,并为程序员编写自己的分析提供了一个 API。
基于云的工具集成了现有的构建和发布过程,并且可以跨多种编程语言工作。与静态分析有关的高级协议也出现了,语言服务器协议(Language Server Protocol)是一套通用的定义,标准化了分析工具与文本编辑器(如 Emacs 和 VS Code)的接口,确保了分析工具可以与工作流集成。同样,SARIF (Static Analysis Results Interchange Format)为静态分析工具生成的输出提供了一个标准,而以检测安全漏洞为目标的静态分析持续吸引着大家的关注。
随着新的平台、操作系统和编程语言的出现,软件生产的机制变得更加复杂。事实上,软件的复杂性增长速度通常快于工程师管理这种复杂性的能力,这是软件工程和计算机科学面临的主要挑战,而静态分析是最有效的武器之一,产生我们这些从业者应得的知识和洞察力。