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

Chr*_*ens 1 .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
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,此代码运行时,我们得到了什么,我期待的事情发生,那种. callvirt将检查是否为null,call但是,这里不会NullReferenceException抛出调用a .这对我来说并不清楚,正如我所期待的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
Run Code Online (Sandbox Code Playgroud)

令我惊讶的是,这一次运行并打印3到控制台,成功退出.我在null引用上调用一个函数,它正在执行.我的猜测是它与没有调用实例字段这一事实有关,因此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
Run Code Online (Sandbox Code Playgroud)

现在,您希望此代码能做什么?我希望代码写出3来到控制台,从控制台读取一个键,然后失败NullReferenceException.嗯,没有发生这种情况.相反,除了a之外,屏幕上不会打印任何值System.AccessViolationException.为什么不一致?

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

1)MSDN列表callvirt将抛出一个NullReferenceExceptionif obj为null,但call只是说它不能为null.为什么然后,它是默认抛出NRE而不是访问冲突?在我看来,call通过契约将尝试访问内存并失败,而不是callvirt通过先检查null 来做什么.

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

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


该"行为是不明确的"答案滚滚之前,我知道这是不是在所有的东西写一个适当的方式,我只是希望有人能帮助阐明上述问题的一些见解.

谢谢.

Han*_*ant 5

1)处理器提高访问冲突.CLR 根据异常的访问地址捕获异常并对其进行转换.地址空间的前64KB内的任何访问都将作为托管NullReferenceException重新引发.请查看此答案以供参考.

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

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