为啥 *(int*)0=0 不会导致访问冲突?

Posted

技术标签:

【中文标题】为啥 *(int*)0=0 不会导致访问冲突?【英文标题】:Why doesn't *(int*)0=0 cause an access violation?为什么 *(int*)0=0 不会导致访问冲突? 【发布时间】:2012-01-30 15:39:27 【问题描述】:

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

【问题讨论】:

(int*)0 是一个空指针。我完全期待NullReferenceException。如果你想要AccessViolationException,试试(int*)0x10(或者可能是0xf0000000)。 Why is memory access in the lowest address space (non-null though) reported as NullReferenceException by .NET?的可能重复 【参考方案1】:

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

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

如果无效访问是对底部 64K 内存的访问,则为 null ref 异常。否则为无效访问异常。

这解释了为什么取消引用 0 和 1 会产生 null ref 异常,以及为什么取消引用 -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【参考方案2】:

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

【讨论】:

【参考方案3】:

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

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

【讨论】:

这并不完全正确。 WriteInt32 确实取消引用指针,但捕获 NRE 并引发访问冲突。 这个答案是错误的。正确答案在这里:***.com/a/7940659/162396【参考方案4】:

MSDN 说得很清楚:

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

请参阅AccessViolationException 帮助。

【讨论】:

这并不能解释为什么两个 sn-ps 会抛出不同的异常。 丹尼尔,其实是在解释。我没有详细说明,因为 JSPerfUnkn0wn 已经有详细的解释。 丹尼尔,你提到的汉斯的解释听起来很合理。【参考方案5】:

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

【讨论】:

以上是关于为啥 *(int*)0=0 不会导致访问冲突?的主要内容,如果未能解决你的问题,请参考以下文章

访问冲突 - 为啥基类析构函数被调用两次? [关闭]

如何从不同的线程访问对象而不会发生冲突?

c语言访问内存冲突,这该怎么办啊

单片机的程序存储器和数据存储器共处同一地址空间为啥不会发生总线冲突?

字符串长度导致访问冲突值 -Visual Studio C++

SIGSEGV 内存访问冲突