为什么*(int*)0=0不会引起访问冲突?

26

出于教育目的,我正在编写一组方法,使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呢?


1
(int*)0 是一个空指针。我完全预料到会出现 NullReferenceException 异常。如果你想要一个 AccessViolationException 异常,可以尝试使用 (int*)0x10(或者可能是 0xf0000000)。 - cHao
5个回答

30

当您对一个空指针进行解引用时,会发生空引用异常;CLR 并不关心空指针是一个带有整数零的不安全指针还是一个带有零的托管指针(即引用类型对象的引用)。

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

如果无效访问是对内存底部 64K 的访问,则为 null 引用异常。否则,它是一个无效访问异常。

这就解释了为什么对零和一进行解引用会导致空引用异常,而对于-1 的解引用会导致无效访问异常;在 32 位机器上,-1 是指针 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] 显然看起来像是对一个空指针进行了解引用操作!因此报告了该错误。


4

这不是一个特定的答案,但是如果您反编译WriteInt32,您会发现它捕获了NullReferenceException并抛出了一个AccessViolationException。因此,行为很可能是相同的,但是由于捕获了真正的异常并引发了不同的异常,所以被掩盖了。


2
NullReferenceException 表示,“当尝试取消引用 null 对象引用时抛出的异常”,因此,由于 *(int*)0 = 0 试图使用对象取消引用来设置内存位置 0x000,它将抛出一个NullReferenceException。请注意,在尝试访问内存之前就会引发此异常。
另一方面,AccessViolationException 类声明:“当尝试读取或写入受保护的内存时抛出的异常”,由于 System.Runtime.InteropServices.Marshal.WriteInt32(IntPtr.Zero, 0) 不使用取消引用,而是使用此方法来设置内存,不会取消引用对象,因此,意味着不会抛出NullReferenceException

1
这并不完全正确。WriteInt32确实会取消引用指针,但会捕获NRE并抛出访问冲突异常。 - Daniel
这个答案是错误的。正确的答案在这里:https://dev59.com/dWsz5IYBdhLWcg3wYGoz#7940659 - Daniel

1

MSDN明确指出:

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

请参阅AccessViolationException帮助。


这并没有解释为什么这两个代码片段会抛出不同的异常。 - Daniel
丹尼尔,实际上它已经解释了。我没有详细说明,因为JSPerfUnkn0wn已经有了详细的解释。 - Yakeen
Daniel,你所提到的Hans的解释听起来很合理。 - Yakeen

0

这就是CLR的工作原理。它不会为每个字段访问检查对象地址是否为null,而是直接访问它。如果它是null,CLR会捕获GPF并像NullReferenceException一样重新抛出它。无论引用类型是什么。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接