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

7

这是一个与在编译时检测除零异常的问题相关的问题。

根据Eric Lippert的回答,要正确实现这个目标是很困难的(我想这也是为什么它尚未提供的原因)。

我的问题是:

不管语言“级别”(高级还是低级),执行此类检查的难度是否相同?

具体来说,C#编译器将C#转换为MSIL。在MSIL级别完成这些检查是否更容易或更困难,作为某种第二遍检查?

还是,语言本身几乎没有任何区别?

阅读Eric的答案中列出的陷阱,我认为这些检查在任何语言中都必须相同?例如,在许多语言中都可以存在跳转,因此需要实现Eric所描述的流程检查...?

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


一般来说,使用低级语言编程比使用高级语言更难。我猜这个情况也不例外。 - Zohar Peled
我知道编程很有规律,但我想知道分析是否也遵循同样的规则?低级语言具有较少的结构和抽象。这使得分析可能的结果更容易、更困难还是根本没有区别?我试图将其可视化。除法最终变成了两次推送和一个操作码。但是你要推送什么可以是任何东西。随着可能命令减少,问题变得不那么复杂,还是说它并没有什么影响,因为问题实际上是关于流检测的。我无法决定... - J M
2
分析高层概念比低层概念更容易。考虑尝试根据食谱确定蛋糕的过敏风险,而不是在没有食谱的情况下拿到一个成品蛋糕。 - Raymond Chen
感谢您的回复并理解了比喻,但我仍然有些困惑。在这种情况下,您有两个食谱(C#和MSIL)。其中一个指令更少,但指令粗略(请原谅非技术术语)。另一个则具有更多的细粒度指令。任何一种情况都会影响检测配方位的两个操作数是否兼容的难度吗? - J M
2
@RaymondChen: 虽然我明白你的观点,但我们可以运用你的类比来引导我们想到另一方面; 如果你把制成品蛋糕放入热卡仪而不是把食谱放入热卡仪,就能更准确地确定一个蛋糕的卡路里含量。:-) 我曾经从事静态分析器的工作,它们在字节码语言中表现得非常好,因为编程语言的抽象化妨碍了看到缺陷的能力,而不是增强了这种能力。 - Eric Lippert
1个回答

13
这是一个非常有趣且深入的问题——尽管它可能不太适合于这个网站。如果我理解正确,问题是:在进行静态分析以寻找缺陷时,选择何种语言进行分析会产生何种影响;应该查看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


1
感谢您抽出时间回复。我正在阅读关于C#内部机制的各种书籍,并且一直在查看自己代码生成的MSIL。学习新概念时显而易见的问题是我如何运用所学知识为团队带来好处。缺陷检测经常被提及。我们使用JetBrains工具,但还没有涉及Roslyn。我突然想到,我们一直拥有C#和MSIL,那么我们未曾利用的能力是什么?您给了我很多思考的空间,特别感谢提供的链接作为研究路径。还需要更多的阅读。 - J M

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接