为什么*(int *)0 = 0导致访问冲突?

出于教育目的,我正在编写一组方法,这些方法会导致C#中的运行时exception,以了解所有exception是什么以及导致它们的原因。 现在,我正在修补导致AccessViolationException程序。

对我来说最明显的方法是写入受保护的内存位置,如下所示:

 System.Runtime.InteropServices.Marshal.WriteInt32(IntPtr.Zero, 0); 

正如我所希望的那样,这AccessViolationException了一个AccessViolationException 。 我想更简洁地做到这一点,所以我决定编写一个带有不安全代码的程序,并通过为零指针赋值来做(我认为的)完全相同的事情。

 unsafe { *(int*)0 = 0; } 

由于无法理解的原因,这会抛出NullReferenceException 。 我玩了一些,发现使用*(int*)1而不是抛出NullReferenceException ,但如果使用负数,如*(int*)-1 ,它将抛出一个AccessViolationException

这里发生了什么? 为什么*(int*)0 = 0会导致NullReferenceException ,为什么它不会导致AccessViolationException

取消引用空指针时会发生空引用exception; CLR不关心空指针是否是一个带有整数零点的不安全指针,或者是一个托管指针(即对引用类型的对象的引用),其中没有插入零。

CLR如何知道null已被解除引用? CLR如何知道其他一些无效指针何时被解除引用? 每个指针指向进程的虚拟内存地址空间中的虚拟内存页面中的某个位置。 操作系统跟踪哪些页面有效以及哪些页面无效; 当您触摸无效页面时,它会引发CLR检测到的exception。 然后,CLR将其表示为无效访问exception或空引用exception。

如果无效访问是内存的底部64K,则它是空引用exception。 否则,它是无效的访问exception。

这解释了为什么解引用零和一个给出空引用exception,以及为什么解除引用-1会给出无效的访问exception; -1是32位机器上的指针0xFFFFFFFF,并且该特定页面(在x86机器上)始终保留供操作系统用于其自身目的。 用户代码无法访问它。

现在,您可以合理地问为什么不仅仅为指针零执行空引用exception,而对其他所有内容执行无效访问exception? 因为在大多数情况下,当一小部分被解除引用时,这是因为你通过空引用来实现它。 想象一下,例如你试图这样做:

 int* p = (int*)0; int x = p[1]; 

编译器将其转换为道德等同于:

 int* p = (int*)0; int x = *( (int*)((int)p + 1 * sizeof(int))); 

这是解除引用4.但从用户的角度来看, p[1]肯定看起来像是null的解引用! 这就是报告的错误。

这本身不是答案,但是如果你反编译WriteInt32你会发现它捕获NullReferenceException并抛出一个AccessViolationException 。 所以行为可能是相同的,但被捕获的真实exception和引发的exceptionexception掩盖了。

NullReferenceException指出“尝试取消引用空对象引用时抛出的exception” ,因此*(int*)0 = 0尝试使用对象取消引用设置内存位置0x000,它将抛出NullReferenceException 。 请注意,在尝试访问内存之前会抛出此exception。

另一方面, AccessViolationException类声明“尝试读取或写入受保护的内存时抛出的exception” ,并且因为System.Runtime.InteropServices.Marshal.WriteInt32(IntPtr.Zero, 0)不使用取消引用,而不是尝试使用此方法设置内存,对象不会被解除引用,因此意味着不会抛出NullReferenceException

MSDN清楚地说:

在完全由可validation的托管代码组成的程序中,所有引用都是有效的或无效的,并且访问违规是不可能的。 仅当可validation的托管代码与非托管代码或不安全的托管代码交互时,才会发生AccessViolationException。

请参阅AccessViolationException帮助。

这就是CLR的工作方式。 它不是为每个字段访问检查对象地址== null,而是访问它。 如果它为null – CLR捕获GPF并像NullReferenceException一样重新抛出它。 无论是什么样的参考。