为什么't *(int*)0=0不会导致访问冲突

本文关键字:访问冲突 int 为什么 | 更新日期: 2023-09-27 18:19:02

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

为什么't *(int*)0=0不会导致访问冲突

当对空指针解引用时,会发生空引用异常;CLR并不关心空指针是插入整型零的不安全指针,还是插入零的托管指针(即对引用类型对象的引用)。

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

如果无效访问是在内存的底部64K,它是一个空ref异常。否则为无效访问异常。

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

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

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。因此,行为可能是相同的,但被捕获的实际异常和引发的不同异常所掩盖。

NullReferenceException声明"当试图解引用一个空对象引用时抛出的异常",所以由于*(int*)0 = 0试图使用对象解引用来设置内存位置0x000,它将抛出NullReferenceException。注意,这个异常是在试图访问内存之前抛出的。

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

MSDN说得很清楚:

在完全由可验证托管代码组成的程序中引用要么有效,要么为空,而访问违反则为空不可能的。AccessViolationException仅在可验证时发生托管代码与非托管代码或不安全的托管代码交互代码。

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