DivideByZeroException 编译器检查复杂性:在 MSIL 与 C# 中更容易或更难,还是没有区别?

DivideByZeroException compiler check complexity: easier or harder in MSIL vs C# or no difference?

这是一个与 this fascinating question about detecting divide by zero exceptions at compile time 有关的问题。

根据 Eric Lippert 的回答,正确实现这一点并非易事(我想这就是为什么尚未提供)。

我的问题是:

无论 "level" 语言是什么,进行这些类型检查的难度是否相同?高等级 vs 低等级?

具体来说,C# 编译器将 C# 转换为 MSIL。作为某种二次通过检查的一部分,这些类型的检查在 MSIL 级别会更容易还是更难?

或者,语言本身的差别很小吗?

阅读 Eric 的回答中列出的陷阱,我认为检查在任何语言中都必须相同?例如,您可以使用多种语言进行跳转,因此需要实施 Eric 描述的流程检查...?

为了让这个问题更具体,这种检查在 MSIL 中比在 C# 中更容易还是更难?

这是一个非常有趣和深刻的问题 -- 虽然可能不太适合这个站点。

问题是,如果我理解的话,在进行静态分析以寻找缺陷时,对分析语言的选择有何影响;分析器应该看 IL,还是应该看源代码?请注意,我已经将这个问题从原来狭隘地关注被零除缺陷扩大了。

答案当然是:视情况而定。这两种技术在静态分析行业中都很常用,并且各有利弊。这取决于您要寻找的缺陷、您使用什么技术来伪装运行错误路径、抑制误报和推断缺陷,以及您打算如何向开发人员揭示发现的缺陷。

分析字节码比源代码有一些明显的好处。最主要的是:如果你有 Java 字节码的字节码分析器,你可以 运行 Scala 通过它而无需编写 Scala 分析器。如果您有 MSIL 分析器,则可以 运行 C# 或 VB 或 F# 通过它,而无需为每种语言编写分析器。

在字节码级别分析代码也有好处。当您拥有字节码时,分析控制流非常容易,因为您可以非常快速地将字节码块组织成 "basic blocks";基本块是一个代码区域,其中没有分支到其中间的指令,并且块的每个正常出口都在其底部。 (异常当然可以发生在任何地方。)通过将字节码分解成基本块,我们可以计算出一个相互分支的块图,然后根据每个块对局部和全局状态的操作来总结每个块。字节码很有用,因为它是对代码的抽象,可以在较低级别显示实际发生的情况。

这当然也是它的主要缺点; 字节码丢失了关于开发者意图的信息。当 运行 字节码时,任何需要源代码信息以检测缺陷或防止误报的缺陷检查器都会给出糟糕的结果。例如考虑一个 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:

等等。 (除了它比那复杂得多。)然后这将被降低为更抽象的字节码;想象一下,试图在开关级别理解这段代码被降低到 gotos,委托降低到闭包。

分析等效字节码的静态分析器完全有权利说 "plainly x can be null because we check for it on one branch of the switch; this is evidence that x must be checked for nullity on other branches, and it is not, therefore I will give a null dereference defect on the other branches"。

但那将是误报。我们知道静态分析器可能不知道的事情,即零状态 必须 在所有其他状态之前执行,并且 当协程恢复时 x 将始终已检查是否为 null。这从原始源代码中显而易见,但很难从字节码中梳理出来。

如果您希望获得字节码分析的好处而没有缺点,您会怎么做?有多种技术;例如,您可以编写自己的比字节码级别更高的中间语言——它具有像 "yield" 或 "await" 或 "for loop" 这样的高级结构——编写一个分析器来分析该中间语言,然后编写编译器,将每种目标语言——C#、Java,无论什么——编译成你的中间语言。这意味着编写很多编译器,但只编写一个分析器,也许编写分析器是最困难的部分。

我知道这是一次非常简短的讨论。这是一个复杂的主题。

如果您对字节码静态分析器的设计感兴趣,请考虑研究 Infer 的设计,这是一种用于 Java 和其他语言的开源静态分析器,可将 Java 字节码转换为偶数适用于分析堆属性的低级字节码;首先阅读用于推断堆属性的分离逻辑。 https://github.com/facebook/infer