为什么这段不安全的代码会抛出NullReferenceException异常?

15

我在为Code Golf上的一个问题尝试使用不安全代码,但我发现了一个无法解释的问题。以下是我的代码:

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

出现访问冲突(段错误)导致崩溃,但是这段代码:

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

抛出一个NullReferenceException异常。在我看来,第一个是进行读取操作,第二个是进行写入操作。异常告诉我,在CLR中的某处拦截了写入操作,并在操作系统杀死该进程之前停止了它。为什么对写入操作会发生这种情况,而对读取操作则没有?如果我将指针值设置得足够大,则会在写入时发生段错误。这是否意味着CLR知道有一块内存是保留的,并且甚至不会尝试写入它?那么,为什么它允许我尝试从该块中读取数据?我完全误解了什么吗?

编辑:

有趣的是:System.Runtime.InteropServices.Marshal.WriteInt32(IntPtr.Zero, 0);导致访问冲突,而不是NullReference。


2
在我的电脑上,两者都会抛出“NullReferenceException”异常。 - user703016
1
猜测:第二个会抛出NullReferenceException,因为编译器能够确定它将始终执行此操作,并用throw语句替换了异常。 - Mike Nakis
1
@MikeNakis:我向你保证,编译器并不那么复杂。 - Eric Lippert
4
这两个代码片段都会导致未受管控的AccessViolation异常(异常代码0xc0000005),然后被.NET异常处理代码转换为NullReferenceException,因为访问发生在虚拟内存地址空间的较低64KB内。很难猜测为什么第一个片段不会出现NRE。使用*(int*)-1会得到AVE。更多信息请参考这里:https://dev59.com/dWsz5IYBdhLWcg3wYGoz#7940659。 - Hans Passant
@EricLippert 哦,好的。我想现在我可以自认为有了可靠的权威。 - Mike Nakis
1个回答

5
第一个异常当然是有道理的——您试图从内存地址0读取。第二个异常有点更有趣。在C ++中,有一个名为NULL的宏/常量,其值为0。它用于无效的指针地址,就像C#中引用类型的null值一样。由于C#引用在内部是指针,因此当您尝试从该地址(地址NULL或0)读取/写入时,会出现NullReferenceException - 实际上,在所有进程中(在Windows中),从0到64K的地址都是无效的,以便捕获程序员的错误。确切的异常或错误可能因计算机硬件或Windows / .NET Framework版本而异,但两个代码片段都应该会导致错误。 至于在读取/写入随机地址时发生的segfault,这是由于操作系统对每个进程的隔离所致。您不能合法地摆弄其他进程的代码或数据。

我也曾这样想,但许多值都会导致NullReferenceException异常,而不仅仅是零。我在高达100k左右的值上遇到了这个问题。之后,一些值可以正常执行(我认为它们在我的程序内存空间中,但我不确定),而其他值则会引发AccessViolationExceptions异常。 - captncraig
1
为了在开发周期的早期捕捉程序员的错误,所有进程中从0到64K的虚拟地址都是无效的。我还假设有效地址在您进程的保留内存中。 - GGulati
这很有道理,“*(int*)65536 = 0”是成功运行的第一个值。 - captncraig
2
@CMP:解引用0x00000004,也就是空引用异常,因为最有可能出现解引用4的情况是类似于int* x = &intArray[1]; int y = *x;这样的代码。如果int数组为空,则其第0个元素将位于0处,其第1个元素将位于4处。解引用4通常是因为您通过空引用到达了那里。 - Eric Lippert
3
对于在随机地址读取/写入时出现的段错误,这是由操作系统对每个进程的隔离所导致的。你不能随意更改其他进程的代码或数据 - 至少不是合法的。使用普通的读/写操作访问的用户模式地址都属于你自己的进程。每个进程都有自己的虚拟内存。要访问其他进程的内存,你需要使用“Read/WriteProcessMemory”。 - CodesInChaos

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