为什么 *(int*)0=0 不会导致访问冲突?
出于教育目的,我正在编写一组导致 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
?
For educational purposes, I'm writing a set of methods that cause runtime exceptions in C# to understand what all the exceptions are and what causes them. Right now, I'm tinkering with programs that cause an AccessViolationException
.
The most obvious way (to me) to do this was to write to a protected memory location, like this:
System.Runtime.InteropServices.Marshal.WriteInt32(IntPtr.Zero, 0);
Just as I had hoped, this threw an AccessViolationException
. I wanted to do it more concisely, so I decided to write a program with unsafe code, and do (what I thought was) exactly the same thing by assigning 0
to the zero-pointer.
unsafe
{
*(int*)0 = 0;
}
For reasons that elude me, this throws a NullReferenceException
. I played around with it some and found out that using *(int*)1
instead also throws a NullReferenceException
, but if you use a negative number, like *(int*)-1
it will throw an AccessViolationException
.
What's going on here? Why does *(int*)0 = 0
cause a NullReferenceException
, and why doesn't it cause an AccessViolationException
?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
当您取消引用空指针时,会发生空引用异常; CLR 不关心空指针是插入整数零的不安全指针还是插入零的托管指针(即对引用类型对象的引用)。
CLR 如何知道 null 已被取消引用? CLR 如何知道其他无效指针何时被取消引用?每个指针都指向进程虚拟内存地址空间中虚拟内存页中的某个位置。操作系统会跟踪哪些页面有效,哪些页面无效;当您触摸无效页面时,它会引发 CLR 检测到的异常。然后,CLR 将其显示为无效访问异常或空引用异常。
如果无效访问内存底部 64K,则为空引用异常。否则为无效访问异常。
这解释了为什么取消引用 0 和 1 会给出 null ref 异常,以及为什么取消引用 -1 会给出无效访问异常; -1 是 32 位机器上的指针 0xFFFFFFFF,并且该特定页面(在 x86 机器上)始终保留给操作系统用于其自身目的。用户代码无法访问它。
现在,您可能会合理地问,为什么不只对指针零执行空引用异常,并对其他所有内容执行无效访问异常?因为大多数情况下,少量数字被取消引用是因为您通过空引用获得了它。想象一下,例如,您尝试执行以下操作:
编译器将其翻译为道德上的等价物:
即取消引用 4。但从用户的角度来看,
p[1]
肯定看起来像是对 null 的取消引用!这就是报告的错误。A null reference exception happens when you dereference a null pointer; the CLR does not care whether the null pointer is an unsafe pointer with the integer zero stuck into it or a managed pointer (that is, a reference to an object of reference type) with zero stuck into it.
How does the CLR know that null has been dereferenced? And how does the CLR know when some other invalid pointer has been dereferenced? Every pointer points to somewhere in a page of virtual memory in the virtual memory address space of the process. The operating system keeps track of which pages are valid and which are invalid; when you touch an invalid page it raises an exception which is detected by the CLR. The CLR then surfaces that as either an invalid access exception or a null reference exception.
If the invalid access is to the bottom 64K of memory, it's a null ref exception. Otherwise it is an invalid access exception.
This explains why dereferencing zero and one give a null ref exception, and why dereferencing -1 gives an invalid access exception; -1 is pointer 0xFFFFFFFF on 32 bit machines, and that particular page (on x86 machines) is always reserved for the operating system to use for its own purposes. User code cannot access it.
Now, you might reasonably ask why not just do the null reference exception for pointer zero, and invalid access exception for everything else? Because the majority of the time when a small number is dereferenced, it is because you got to it via a null reference. Imagine for example that you tried to do:
The compiler translates that into the moral equivalent of:
which is dereferencing 4. But from the user's perspective,
p[1]
surely looks like a dereference of null! So that is the error that is reported.这本身并不是一个答案,但如果您反编译
WriteInt32
,您会发现它捕获NullReferenceException
并抛出AccessViolationException
。因此,行为可能是相同的,但被捕获的真正异常和引发的不同异常所掩盖。This isn't an answer per se, but if you decompile
WriteInt32
you find it catchesNullReferenceException
and throws anAccessViolationException
. So the behavior is likely the same, but is masked by the real exception being caught and a different exception being raised.NullReferenceException 声明 < em>“尝试取消引用空对象引用时引发的异常”,因此由于
*(int*)0 = 0
尝试设置内存位置0x000 使用对象取消引用会抛出 NullReferenceException。请注意,在尝试访问内存之前会抛出此异常。另一方面, AccessViolationException 类指出, “尝试读取或写入受保护的内存时引发的异常”,并且由于 System.Runtime.InteropServices.Marshal.WriteInt32(IntPtr.Zero, 0) 不使用取消引用,而是尝试使用此方法设置内存,对象不会取消引用,因此意味着不会抛出
NullReferenceException
。The NullReferenceException states that "The exception that is thrown when there is an attempt to dereference a null object reference", so since
*(int*)0 = 0
tries to set memory location 0x000 using an object dereference it will throw aNullReferenceException
. Note that this Exception is thrown before trying to even access the memory.The AccessViolationException class on the other hand states that, "The exception that is thrown when there is an attempt to read or write protected memory", and since
System.Runtime.InteropServices.Marshal.WriteInt32(IntPtr.Zero, 0)
does not use a dereference, instead tries to set the memory using this method, an object is not dereferenced, therefore meaning noNullReferenceException
will be thrown.MSDN 说得很清楚:
请参阅 AccessViolationException 帮助。
The MSDN says that clearly:
See AccessViolationException help.
这就是 CLR 的工作原理。它不会检查每个字段访问的对象地址是否为 null,而是直接访问它。如果它是 null - CLR 捕获 GPF 并像 NullReferenceException 一样重新抛出它。不管它是什么样的参考。
This is how CLR work. Instead of checking if object address == null for every field access, it just access it. If it was null - CLR catches GPF and rethrow it like NullReferenceException. No matter what kind of reference it was.