什么是落后提高/生成一个空引用异常的CLR实现?(What is the CLR implement

2019-06-25 20:04发布

我们确实遇到过这种特殊的和我们的编码/开发生命一天或一天​​中最常见的例外之一。 我的问题不是关于为什么 (我知道它提出,当我们试图访问它实际上指向空引用变量的性质),但它是关于如何由CLR所产生的空引用异常。

有时候,我不得不想到机制来标识一个空的引用(也许是空是内存中预留空间),然后通过CLR引发异常。 CLR如何识别并引发此特定的异常。 OS是否在它起什么作用?

我想和大家分享一下最有趣的要求之一:

其实是众所周知的CLR所有的时间保留的内存空间,以及所有类型的访问被禁止。 因此,当该空间参考被发现,它由缺省生成拒绝访问种类经由OS异常这被解释为通过CLR一个空引用异常的。

我没有找到任何文章或帖子支持上述说法,因此很难相信它。 可以通过我的思念在细节或其他原因去挖掘,我希望#2是最合适的平台,在这里我会得到最好的回应之一。

Answer 1:

它不必是(可能有明确的检查),但它从捕获访问冲突异常的工作。

.NET对象将变成一个本地对象:其字段变得以特定方式布置的存储器块,其方法是即时编译成本机代码的方法,并创建一个v表或其他虚拟方法过载机构。

  1. 访问字段然后,意味着找到该对象的地址,添加所述偏移构件的,和读取或写入的一块内存提及。

  2. 调用虚方法,意味着找到该对象的地址,找到其方法表(集对象内的偏移),找到该方法的地址(设定表内的偏移量),并调用在与所述对象的地址的地址的方法被传递(在this指针)。

  3. 调用非虚拟方法,意味着调用带有(传递的对象的地址的方法this指针)。

显然,如果没有在有关案件的地址的实际对象1和2会出差错以某种方式,而案例3就可以了(但可能反过来导致事件1或2)。 有两个主要方面,这可能出问题:

  1. 它可以访问内存的任意位是不是真的我们的类型的对象,从而导致各种令人兴奋的和真的很难追查错误(.NET代码一般不会导致任何导致这种情况下)。

  2. 它可以访问受保护的内存作为任意位,导致访问冲突。

你可能知道从C,C ++或ASM编码的第二种情况。 如果没有,你可能还看到一个程序崩溃,并用其奄奄一息谈访问冲突的一些地址。 如果是这样,你可能已经注意到,虽然给出的地址可以是任何东西,它会经常是要么00000000或某事非常低的像0x00000020。 这些是由代码试图取消引用空指针是否访问一个字段或者调用虚方法(这基本上访问字段,然后根据你所得到的调用)引起的。

现在,由于第一64K或存储器始终受到保护,解除引用一个空指针将总是导致在第二种情况下(访问冲突),而不是第一种情况(任意的存储器被误用和造成的错误离奇“在芯丹戈” )。

这是所有恰与.NET相同(或更确切地说,与由它产生的即时编译代码),但是,如果(A)的访问冲突发生在比0x00010000在和下一个地址(B)这样的违反被发现已发生了由这是即时编译代码,那么它被变成NullReferenceException ,否则它就会变成一个AccessViolationException

我们可以用代码无法取消引用模拟这一点,但它确实访问受保护的内存(我们将只读的,所以如果我们应该发生不小心碰到内存不被保护,结果也不会太奇怪了!) :

下面的代码将引发AccessViolationException:

unsafe
{
  int read = *((int*)long.MaxValue - 8);
}

下面的代码将引发一个NullReferenceException:

unsafe
{
  int read = *((int*)8);
}

无论是代码实际上是取消引用任何东西。 这两个原因访问冲突,但CLR假设后来可能是由一个空引用引起的(公平地说,目前最可能的情况),并提高了它。

所以,我们可以看到现场的访问和如何callvirt可能导致此。

值得一提的,现在因为一个决定,不允许C#调用上的空引用的方法,即使安全的这样做, callvirt作为IL为广大C#的情况下,唯一的例外是静态方法病例或它可以在编译时被证明不会对空引用。 (编辑:还有一些其他的情况下,编译器可以看到一个callvirt可以取而代之的是一个call ,即使方法实际上是虚拟的[如果编译器可以知道哪些超载会被击中]和后来的编译器会做到这一点往往略多,但它仍然会使用callvirt往往比你想象的)。

一个有趣的情况是优化意味着一个调用方法callvirt会内联,但它不是在编译时已知保证非空。 在这种情况下,现场访问可能哪里哪里“呼叫”(这是不是一个真正的呼叫)发生,恰恰触发位置之前,必须添加NullReferenceException在开始,而不是在中间,方法。 这意味着优化不改变所观察到的行为。



Answer 2:

该MS执行,IIRC,通过访问冲突这一点。 空本质上是一个零参考,并基本上是:他们故意保留该地址空间,离开这个页面未映射。 内存访问冲突是在自动的CPU / OS水平升高(即不需要额外的代码做一个空检查),而CLI然后报告这是一个空引用异常。

有趣的是,因为内存中的页面处理,你可以真正模拟(如果你足够努力)一个空引用异常的非零但价值较低,出于同样的原因。

编辑:埃里克利珀讨论此此相关的问题/回答: https://stackoverflow.com/a/8681563



Answer 3:

您已经阅读CLI规格- ECMA-335 ? 你会找到一些答案在那里。

类11个语义 ...当具有作为它的类型,创建(例如,通过调用具有类类型的局部变量的方法)的类的变量或字段,该值最初应为空,一个特殊的值即:=用,即使它不属于任何特定类的实例的所有类类型。

ldnull指令的描述:

所述ldnull推堆栈上的空引用(类型O)。 这是用来他们成为活的或当他们成为死之前初始化位置。 [理由:它可能会认为ldnull是多余的:为什么不使用ldc.i4.0或ldc.i8.0呢? 答案是ldnull提供了一个大小无关的空 - 类似于一个ldc.i指令,这不存在。 然而,即使是CIL到包括ldc.i指令它仍然会受益验证算法保留ldnull指令,因为它使键入跟踪更容易。 端基本原理] 可验证 :该ldnull指令总是核查,并产生零型(§1.8.1.2)的一个值,该值是分配到(§I.8.7.3)的任何其它引用类型。



文章来源: What is the CLR implementation behind raising/generating a null reference exception?