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

时间:2011-12-30 15:34:31

标签: c# pointers access-violation unsafe

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

的程序

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

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

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

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

由于无法理解的原因,这会引发NullReferenceException。我玩了一些,发现使用*(int*)1代替也会抛出NullReferenceException,但如果使用负数,例如*(int*)-1则会抛出AccessViolationException

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

5 个答案:

答案 0 :(得分:29)

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

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

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

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

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

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

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

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

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

答案 1 :(得分:4)

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

答案 2 :(得分:2)

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

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

答案 3 :(得分:1)

MSDN明确表示:

  

在完全由可验证的托管代码组成的程序中,全部   引用有效或为null,并且访问违规是   不可能。只有在可验证时才会发生AccessViolationException   托管代码与非托管代码或不安全的托管代码交互   代码。

请参阅AccessViolationException帮助。

答案 4 :(得分:0)

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