在空引用上调用实例方法有时会成功



很抱歉有这么多的文字,但我想给出一个很好的背景情况。我知道你可以在IL中调用null引用上的方法,但仍然不理解当你这样做时发生的一些非常奇怪的事情,关于我对CLR如何工作的理解。我在这里找到的其他几个问题并没有涵盖我在这里看到的行为。

这是一些IL:

.assembly MrSandbox {}
.class private MrSandbox.AClass {
    .field private int32 myField
    .method public int32 GetAnInt() cil managed {
        .maxstack  1
        .locals init ([0] int32 retval)
        ldc.i4.3
        stloc retval
        ldloc retval
        ret
    }
    .method public int32 GetAnotherInt() cil managed {
        .maxstack  1
        .locals init ([0] int32 retval)
        ldarg.0
        ldfld int32 MrSandbox.AClass::myField
        stloc retval
        ldloc retval
        ret
    }
}
.class private MrSandbox.Program {
    .method private static void Main(string[] args) cil managed {
        .entrypoint
        .maxstack  1
        .locals init ([0] class MrSandbox.AClass p,
                      [1] int32 myInt)
        ldnull
        stloc p
        ldloc p
        call instance int32 MrSandbox.AClass::GetAnotherInt()
        stloc myInt
        ldloc myInt
        call void [mscorlib]System.Console::WriteLine(int32)
        ret
    }
}
现在,当这段代码运行时,我们得到了我期望发生的事情,有点像callvirt将检查是否为空,而call不会,然而,在调用时抛出了NullReferenceException。这对我来说不清楚,因为我希望System.AccessViolationException代替。我将在这个问题的最后解释我的理由。

如果我们将Main(string[] args)中的代码替换为以下代码(在.locals行之后):

        ldnull
        stloc p
        ldloc p
        call instance int32 MrSandbox.AClass::GetAnInt()
        stloc myInt
        ldloc myInt
        call void [mscorlib]System.Console::WriteLine(int32)
        ret

令我惊讶的是,这个程序运行了,并将3打印到控制台,成功退出。我在一个空引用上调用一个函数,它正在正确地执行。我的猜测是,这与没有调用实例字段的事实有关,因此CLR可以成功执行代码。

最后,这是我真正困惑的地方,用以下代码替换Main(string[] args)中的代码(在.locals行之后):

        ldnull
        stloc p
        ldloc p
        call instance int32 MrSandbox.AClass::GetAnInt()
        stloc myInt
        ldloc myInt
        call void [mscorlib]System.Console::WriteLine(int32)
        call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
        pop
        call instance int32 MrSandbox.AClass::GetAnotherInt()
        stloc myInt
        ldloc myInt
        call void [mscorlib]System.Console::WriteLine(int32)
        ret

现在,你希望这段代码做什么?我期望代码将3写入控制台,从控制台读取密钥,然后在NullReferenceException上失败。好吧,这些都不会发生。相反,除了System.AccessViolationException外,没有任何值被打印到屏幕上。为什么不一致?

背景完了,下面是我的问题:

1) MSDN列出,callvirt将抛出一个NullReferenceException,如果obj是空的,但call只是说,它必须不是空的。那么,为什么它在默认情况下抛出一个NRE而不是一个访问冲突呢?在我看来,call通过契约将尝试访问内存并失败,而不是像callvirt那样先检查null。

2)第二个例子的原因是由于它访问没有类级别字段和call不做空检查?如果是这样,如何在空引用上调用非静态方法并成功返回?我的理解是,当一个引用类型被放在堆栈上时,只有它放在堆上的type对象。方法是从类型对象调用的吗?

3)为什么第一个和最后一个例子抛出的异常不同?在我看来,第三个例子抛出了正确的异常,一个AccessViolationException,因为这正是它想要做的;访问未分配内存


在"行为未定义"的答案出现之前,我知道这根本不是一种正确的写作方式,我只是希望有人能帮助我对上述问题提供一些见解。

谢谢。

1)处理器引发访问冲突。CLR捕获异常,并且根据异常的访问地址转换异常。地址空间的前64KB内的任何访问都将作为托管NullReferenceException重新引发。参考这个答案。

2)是的,CLR不强制非空this值。例如,c++/CLI编译器生成的代码不执行此检查,这与本地c++非常相似。只要方法不使用this引用,就不会导致异常。c#编译器在调用callvirt方法之前显式地生成代码来验证this的值。参考这篇博文。

3)你得到了错误的IL, GetAnotherInt()是一个实例方法,但你忘了写ldloc指令。你得到一个AV,因为引用指针是随机的。

我不能肯定地回答2),但这里有1)和3)。

NullReferenceExceptionAccessViolationException是一样的;在CLR的早期,根本没有AccessViolationException,对一个无效但非空的指针解引用仍然会得到一个NullReferenceException

这是因为在今天的计算机上,让硬件进行完整性检查的成本更低。你对抛出哪个异常的概念是基于CLR做显式null检查(if (foo == null) throw new NullReferenceException())的想法,但这不是微软在Windows pc上实现的情况。

当你解引用一个无效的地址时,你的程序被中断,因为它做了一些无效的事情;CLR与该中断挂钩,并将根据触发故障的地址抛出NullReferenceExceptionAccessViolationException。这样,它就不需要插入任何内存检查,并且它仍然会以可预测的方式运行。

如果我没记错的话,访问0xFFFF下的任何地址都将导致NullReferenceException,而上面的任何地址都将是AccessViolationException。您可以使用不安全代码和指针进行验证。我自己从来没有在c#中使用过不安全的代码,所以下面的代码片段可能不起作用,但我希望测试所需的修复是微不足道的。(我的一个朋友用。net Framework 3或3.5测试了这个问题,当时它是最新的,所以有可能这些数据不是最新的。)

byte* foo = null;
*foo; // NullReferenceException
byte* bar = 0x10000;
*foo; // AccessViolationException

我对问题2的把握不大:要调用的方法的地址是在编译时确定的,因为它不能改变。callvirt在null引用上出错的原因是它需要访问对象的虚函数表,这样做需要读取对象的头。对于常规的call,由于不需要在运行时确定要调用的方法,因此不需要查找任何内容,CLR可以直接进行。(至少,这大致是c++的工作方式,所以我想它与CLR的工作方式相差不大)

这有点奇怪,因为OP声明PEverify不会失败。

GetAnotherInt的最后一次调用看起来无效。

此时堆栈上没有任何内容。

这至少解释了AccessViolationException; p

不知道为什么PEVerify允许。

更新:

PEVerify确实失败了。

[IL]: Error: MrSandbox.Program::Main][offset 0x00000021] Stack underflow.

最新更新