这是一个非常有趣且深入的问题——尽管它可能不太适合于这个网站。如果我理解正确,问题是:在进行静态分析以寻找缺陷时,选择何种语言进行分析会产生何种影响;应该查看IL还是源代码?请注意,我已经将这个问题从最初狭窄的焦点(即除零缺陷)扩展开来。
答案当然是:取决于情况。静态分析行业通常使用这两种技术,并且每种技术都有其优缺点。这取决于您要查找什么缺陷、使用什么技术来修剪虚假路径、抑制错误阳性和推断缺陷,以及您如何打算向开发人员展示发现的缺陷。
在字节码上进行分析比在源代码上进行分析具有一些明显的好处。其中最重要的好处是:如果您有一个用于Java字节码的字节码分析器,则可以通过它运行Scala,而无需编写Scala分析器。如果您有一个MSIL分析器,则可以通过它运行C#、VB或F#,而无需为每种语言编写分析器。
在字节码级别上进行代码分析也有好处。当你有字节码时,分析控制流程变得非常容易,因为你可以很快地将字节码块组织成“基本块”;一个基本块是一段代码区域,其中没有指令在其中间分支,而每个正常的退出块都位于其底部。(当然,异常可以在任何地方发生。)通过将字节码分解成基本块,我们可以计算出彼此之间分支到达的块的图形,并根据其在局部和全局状态上执行的操作总结每个块。字节码是有用的,因为它是一种低级别的抽象,能够显示正在发生什么。
当然,这也是它的主要缺点;字节码丢失了开发人员的意图信息。任何需要源代码信息才能检测缺陷或防止误报的缺陷检查器在运行字节码时都会产生不良结果。例如考虑一个C程序:
#define DOBAR if(foo)bar();
...
if (blah)
DOBAR
else
baz();
如果这段可怕的代码降到机器码或字节码,那么我们将只会看到一堆分支指令,我们不会知道我们应该在这里报告一个缺陷,也不会知道
else
绑定到
if(foo)
而不是开发者打算的
if(blah)
。
C预处理器的危险是众所周知的。但是,在字节码级别进行复杂代码分析时也会带来很大困难。例如,考虑像C#这样的语言:
async Task Foo(Something x)
{
if (x == null) return;
await x.Bar();
await x.Blah();
}
显然在这里无法将x
作为null解引用。 但是C#将把它降级为一些绝对疯狂的代码; 其中的一部分代码将类似于此:
int state = 0;
Action doit = () => {
switch(state) {
case 0:
if (x == null) {
state = -1;
return;
};
state = 1;
goto case 1:
case 1:
Task bar = x.Bar();
state = 2;
if (<bar is a completed task>) {
goto case 2;
} else {
<assign doit as the completion of bar>
return;
}
case 2:
所以说,情况比那复杂得多。字节码会被降低到更抽象的程度;想象一下在开关被降低到转到语句和委托被降低到闭包的级别上理解此代码。
分析等效字节码的静态分析器有权明确地表示“明显x可能为空,因为我们在开关的一个分支上对其进行了检查;这表明必须在其他分支上检查x是否为空,而且没有检查,因此我将在其他分支上给出空指针引用缺陷”。
但这是一个误报。我们知道一些静态分析器可能不知道的内容,即零状态必须在每个其他状态之前执行,并且当恢复协同程序时,x将始终已经被检查过了。这从原始源代码中很明显,但从字节码中很难梳理出来。
那么,如果您希望获得字节码分析的好处却又不想面临缺点怎么办呢?有各种技术可供选择;例如,您可以编写自己的中间语言,该语言比字节码高级--具有像“yield”或“await”,或“for loop”这样的高级结构--编写分析器,分析该中间语言,然后编写每个目标语言--C#、Java等--的编译器,将其编译成您的中间语言。这意味着要编写很多编译器,但只需要一个分析器,也许编写分析器是最困难的部分。
我知道这是一个非常简短的讨论。这是一个复杂的主题。
如果您对字节码上的静态分析器设计感兴趣,请考虑了解Infer的设计,它是用于Java和其他语言的开源静态分析器,可以将Java字节码转换为更低级别的字节码,以便分析堆属性;首先阅读有关分离逻辑推断堆属性的内容。https://github.com/facebook/infer