在Null Reference上调用实例方法有时会成功

时间:2011-11-15 18:45:37

标签: .net clr il

对不起文字的墙,但我想提供一个很好的背景情况。我知道你可以在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

令我惊讶的是,这一次运行,并将Main(string[] args)打印到控制台,成功退出。我在null引用上调用一个函数,它正在执行。我的猜测是它与没有调用实例字段这一事实有关,因此CLR可以成功执行代码。

最后,这就是我真正困惑的地方,用.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写入控制台,从控制台读取密钥,然后在callvirt上失败。嗯,没有发生这种情况。相反,除了NullReferenceException之外,屏幕上不会打印任何值。为什么它不一致?

有了背景,以下是我的问题:

1)如果obj为空,MSDN列出call将抛出call,但callvirt只是说它不能为空。为什么然后,它是默认抛出NRE而不是访问冲突?在我看来,call通过契约将尝试访问内存并失败,而不是通过首先检查null来执行AccessViolationException所做的事。

2)第二个例子是否起作用的原因是它没有访问类级字段并且{{1}}没有进行空检查?如果是这样,如何在空引用上调用非静态方法并返回成功?我的理解是,当一个引用类型放在堆栈上时,只有它放在堆上的Type对象。那么从类型对象调用的方法是什么?

3)为什么在第一个和最后一个例子之间存在异常差异?在我看来,第三个例子抛出了正确的异常,{{1}},因为这正是它正在尝试做的事情;访问未分配的内存。


在“行为未定义”答案开始之前,我知道这不是 AT ALL 一种正确的写作方式,我只是希望有人可以帮助提供一些见解以上问题。

感谢。

3 个答案:

答案 0 :(得分:5)

1)处理器 引发访问冲突。 CLR根据异常的访问地址捕获异常并转换它。地址空间的前64KB内的任何访问都将作为托管NullReferenceException重新引发。请查看this answer以供参考。

2)是的,CLR不会强制执行非值。例如,C ++ / CLI编译器生成的代码不执行此检查,就像本机C ++一样。只要该方法不使用 this 引用,就不会导致异常。在方法调用callvirt之前,C#编译器显式生成代码以验证 this 的值。请参阅this blog post以供参考。

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

答案 1 :(得分:1)

我无法回答2)肯定,但这里是1)和3)。

NullReferenceExceptionAccessViolationException相同;在CLR的早期,根本没有AccessViolationException,并且取消引用无效但非空指针仍然提供NullReferenceException

这是因为在今天的计算机上,让硬件进行健全性检查的成本更低。您抛出哪个异常的概念是基于CLR进行显式空检查(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错误的原因是它需要访问对象的vtable,并且这样做需要读取对象的头。使用常规call,因为不需要在运行时确定要调用的方法,所以无需查找,CLR可以直接进行。 (至少,这大致是它对C ++的作用,所以我认为它与CLR的工作原理并不太远。)

答案 2 :(得分:1)

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

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

那一刻堆栈中什么都没有。

至少解释了AccessViolationException; P

不确定为什么PEVerify允许它。

<强>更新

PEVerify确实失败了。

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