当前位置: 首页 > 编程语言 > C#

抛出-生成空引用异常背后的CLR实现是什么?分享

时间:2023-04-10 11:31:45 C#

抛出/生成空引用异常背后的CLR实现是什么?我们确实遇到了这个特殊问题,它是我们编码/开发生命周期或其他方面最常见的例外之一。我的问题不是关于为什么(我知道当我们尝试访问实际指向null的引用变量的属性时它会抛出),而是关于CLR如何生成NULLREFERENCEEXCEPTION。有时我不得不认为用于识别对null的引用的机制(可能null是内存中的保留空间)然后通过CLR抛出异常。CLR如何识别并抛出这个特定的异常。操作系统在这方面有什么作用吗?我想与大家分享一个最有趣的说法:null实际上是CLR已知的一直保留的内存空间,并且禁止各种访问。因此,当找到对该空间的引用时,它默认会通过操作系统生成拒绝访问异常类型,CLR将其解释为NULL引用异常。我还没有找到任何支持上述说法的文章或帖子,所以很难相信。可能是本人挖掘细节不够或者其他原因,希望Stackoverflow是最合适的平台之一,我会得到最好的回应。它不一定是(可能有显式检查),但它可以捕获访问冲突异常。.NET对象成为本机对象:它们的字段成为以特定方式布局的内存块,它们的方法嵌入本机机器代码方法中,并创建v表或其他虚拟方法重载机制。然后,访问一个字段意味着找到对象的地址,添加成员的偏移量,以及读取或写入引用的内存块。调用虚方法就是先找到对象的地址,找到方法表(设置对象中的偏移量),找到方法的地址(设置表中的偏移量),然后调用该地址的方法,传递对象的地址(this指针)。调用非虚拟方法意味着使用传递的对象的地址(this指针)调用该方法。显然,如果所讨论的地址没有实际对象,那么情况1和情况2会以某种方式出错,而情况3会起作用(但可能会导致情况1或情况2反过来)。这有两种主要的错误方式:它可以访问任何非我们类型的对象的任意内存,导致各种令人兴奋且很难追踪的错误(.NET代码通常不会导致任何导致这种情况的错误).它可以访问受保护的任意内存位,从而导致访问冲突。您可能从C、C++或ASM编码中了解第二种情况。如果没有,您可能仍然会看到程序崩溃,并且有一种即将死去的感觉,它在谈论某个地址的访问冲突。如果是这样,您可能已经注意到,虽然给定的地址几乎可以是任何地址,但通常是0x00000000或非常低的地址,例如0x00000020。这些是由试图取消引用空指针的代码引起的,无论是通过访问字段还是调用虚拟方法(实际上是访问字段,然后根据您获得的内容进行调用)。现在,由于第一个64k或内存始终受到保护,取消引用空指针将始终导致第二种情况(访问冲突)而不是第一种情况(任意内存被滥用并导致奇怪的“fandangoonthecore”错误)。这与.NET(或者更确切地说,它生成的jitted代码)完全相同,但是如果(A)访问冲突发生在0x00010000以下的地址,并且(B)发现此类冲突发生在已经jitted的地址代码,则转换为NullReferenceException,否则变为AccessViolationException。我们可以用不取消引用但访问受保护内存的代码来模拟这一点(我们只会读取,所以如果我们碰巧遇到未受保护的内存,结果不会太奇怪!):以下代码将抛出AccessViolationException:不安全{intread=*((int*)long.MaxValue-8);}以下代码将抛出NullReferenceException:unsafe{intread=*((int*)8);这些代码实际上都没有取消引用任何内容。两者都会导致访问冲突,但CLR假定后者可能是由空引用引起的(公平地说,目前为止最有可能的情况)并引发它。所以我们可以看到字段访问和callvirt是如何导致这种情况的。现在值得注意的是,由于决定不允许C#调用空引用上的方法,即使这样做是安全的,在C#中的大多数情况下,callvirt被用作IL,唯一的例外是静态方法或它可能在编译时出现不在空引用上。(编辑:在其他一些情况下,编译器可以看到callvirt可以被call替换,即使该方法实际上是虚拟的[如果编译器可以判断将命中哪个重载],以后的编译器会这样做虽然它仍然会使用callvirt比你想象的更频繁,但更频繁。一个有趣的案例是,优化意味着一个用callvirt调用的方法可以被内联,但在编译时并不知道是否保证它是非空的。在这个中在这种情况下,可以在“调用”(实际上不是调用)发生的点之前添加字段访问,从而在方法的开头而不是中间触发NullReferenceException。这意味着优化不会t改变观察到的行为。MS实现IIRC这是通过访问冲突实现的。空值基本上是零引用,基本上:它们有意保留该地址空间并保持该页面未映射。内存访问冲突在CPU/OS级别自动引发(即空检查不需要额外的代码),然后CLI将此报告为空引用异常。有趣的是,因为内存是按页处理的,你实际上可以模拟(如果你足够努力的话)一个非零但值非常低的空引用异常。编辑:EricLippert在这个相关问题/答案中讨论了这个问题:https://stackoverflow.com/a/8681563您是否阅读了CLI规范-ECMA-335?你会在那里找到一些答案。11类的语义...当创建以类为类型的变量或字段时(例如,通过调用类类型的局部变量的方法),值最初应为空,一个特殊值:=All类类型,即使它不是任何特定类的实例。以及ldnull指令的描述:ldnull将空引用(O类型)压入堆栈。这用于在位置生效或消失之前初始化位置。[原因:可能认为ldnull是多余的:为什么不使用ldc.i4.0或ldc.i8.0?答案是ldnull提供了一个与大小无关的null-类似于ldc.i指令,它不存在。然而,即使CIL包含ldc.i指令,保留ldnull指令对于验证算法仍然是有益的,因为它使类型跟踪更容易。endreasone]有效性:ldnull指令始终有效并生成类型为(§1.8.1.2)null的值,可以将其分配(§I.8.7.3)给任何其他引用类型。以上是C#学习教程:抛出/生成空引用异常背后的CLR实现是什么?如果所有分享的内容对你有用,需要进一步了解C#学习教程,希望大家多多关注。本文收集自网络,不代表立场。如涉及侵权,请点击右侧联系管理员删除。如需转载请注明出处: