CoreCLR源码探索(六) NullReferenceException是如何发生的

NullReferenceException可能是.Net程序员遇到最多的例外了, 这个例外发生的如此频繁,以至于人们付出了巨大的努力来使用各种特性和约束试图防止它发生, 但时至今日它仍然让很多程序员头痛, 今天我将讲解这个令人头痛的例外是如何发生的.

可以导致NullReferenceException发生的源代码

我们先来看看什么样的代码可以导致NullReferenceException发生:

第一份代码, 调用函数时this等于null导致例外发生

using System;
namespace ConsoleApp1{    
class Program{      
 public class MyClass{        
     public int MyMember;    

       public void MyMethod() { }}        static void Main(string[] args)        {MyClass obj = null;obj.MyMethod();}} }

第二份代码, 访问成员时this等于null导致例外发生

using System;

namespace ConsoleApp1{  

 class Program{      
 public class MyClass{        
    public int MyMember;    
       public void MyMethod() { }}        static void Main(string[] args)        {MyClass obj = null;Console.WriteLine(obj.MyMember);}} }

观察生成的IL代码

再来看看生成的IL:

第一份代码的IL

.method private hidebysig static void Main (string[] args) cil managed 
{// Method begins at RVA 0x2050// Code size 11 (0xb).maxstack 1.entrypoint.locals init ([0] class ConsoleApp1.Program/MyClass)IL_0000: nopIL_0001: ldnullIL_0002: stloc.0IL_0003: ldloc.0IL_0004: callvirt instance void ConsoleApp1.Program/MyClass::MyMethod()IL_0009: nopIL_000a: ret
} // end of method Program::Main

第二份代码的IL

.method private hidebysig static void Main (string[] args) cil managed 
{// Method begins at RVA 0x2050// Code size 16 (0x10).maxstack 1.entrypoint.locals init ([0] class ConsoleApp1.Program/MyClass)IL_0000: nopIL_0001: ldnullIL_0002: stloc.0IL_0003: ldloc.0IL_0004: ldfld int32 ConsoleApp1.Program/MyClass::MyMemberIL_0009: call void [System.Console]System.Console::WriteLine(int32)IL_000e: nopIL_000f: ret
} // end of method Program::Main

看出什么了吗? 看不出吧, 我也看不出, 这代表了null检查不是在IL层面实现的, 我们需要继续往下看.

观察生成的汇编代码

看生成的汇编代码:

第一份代码生成的汇编 (架构不同生成的代码也不同, 以下代码是windows x64生成的)

    10:         static void Main(string[] args) {00007FF9F5C30482 56                   push        rsi  00007FF9F5C30483 48 83 EC 30          sub         rsp,30h  00007FF9F5C30487 48 8B EC             mov         rbp,rsp  00007FF9F5C3048A 33 C0                xor         eax,eax  00007FF9F5C3048C 48 89 45 20          mov         qword ptr [rbp+20h],rax  00007FF9F5C30490 48 89 45 28          mov         qword ptr [rbp+28h],rax  00007FF9F5C30494 48 89 4D 50          mov         qword ptr [rbp+50h],rcx  00007FF9F5C30498 83 3D 49 48 EA FF 00 cmp         dword ptr [7FF9F5AD4CE8h],0  00007FF9F5C3049F 74 05                je          00007FF9F5C304A6  00007FF9F5C304A1 E8 1A B5 C0 5F       call        00007FFA5583B9C0  00007FF9F5C304A6 90                   nop      11:             MyClass obj = null;00007FF9F5C304A7 33 C9                xor         ecx,ecx  00007FF9F5C304A9 48 89 4D 20          mov         qword ptr [rbp+20h],rcx      12:             obj.MyMethod();00007FF9F5C304AD 48 8B 4D 20          mov         rcx,qword ptr [rbp+20h]  00007FF9F5C304B1 39 09                cmp         dword ptr [rcx],ecx  00007FF9F5C304B3 E8 E8 FB FF FF       call        00007FF9F5C300A0  00007FF9F5C304B8 90                   nop      13:         }

第二份代码生成的汇编

    10:         static void Main(string[] args) {00007FF9F5C20B22 56                   push        rsi  00007FF9F5C20B23 48 83 EC 30          sub         rsp,30h  00007FF9F5C20B27 48 8B EC             mov         rbp,rsp  00007FF9F5C20B2A 33 C0                xor         eax,eax  00007FF9F5C20B2C 48 89 45 20          mov         qword ptr [rbp+20h],rax  00007FF9F5C20B30 48 89 45 28          mov         qword ptr [rbp+28h],rax  00007FF9F5C20B34 48 89 4D 50          mov         qword ptr [rbp+50h],rcx  00007FF9F5C20B38 83 3D A9 41 EA FF 00 cmp         dword ptr [7FF9F5AC4CE8h],0  00007FF9F5C20B3F 74 05                je          00007FF9F5C20B46  00007FF9F5C20B41 E8 7A AE C1 5F       call        00007FFA5583B9C0  00007FF9F5C20B46 90                   nop      11:             MyClass obj = null;00007FF9F5C20B47 33 C9                xor         ecx,ecx  00007FF9F5C20B49 48 89 4D 20          mov         qword ptr [rbp+20h],rcx      12:             Console.WriteLine(obj.MyMember);00007FF9F5C20B4D 48 8B 4D 20          mov         rcx,qword ptr [rbp+20h]  00007FF9F5C20B51 8B 49 08             mov         ecx,dword ptr [rcx+8]  00007FF9F5C20B54 E8 87 FB FF FF       call        00007FF9F5C206E0  00007FF9F5C20B59 90                   nop      13:         }

从汇编我们可以看出点端倪了, 注意第一份代码中的以下指令

00007FF9F5C304B1 39 09                cmp         dword ptr [rcx],ecx  

和第二份代码中的以下指令

00007FF9F5C20B51 8B 49 08             mov         ecx,dword ptr [rcx+8]  

在第一份代码中多了一个奇怪的cmp指令,
这个cmp比较了rcx自身但是却不使用比较的结果(后续je, jne等等),
这个指令正是null检查的真面目,
rcx寄存器保存的是obj对象的指针, 也是下面的call指令的第一个参数(this),
如果rcx等于0(obj等于null)时, 这条指令就会执行失败.

在第二份代码中mov ecx,dword ptr [rcx+8]指令的作用是把rcx保存的obj的MyMember成员的值移到ecx,
可以理解为c语言的int myMember = obj->MyMember;int myMember = *(int*)(((char*)obj)+8),
这里的8是MyMember距离对象开头的偏移值,
想象一下如果obj等于null, rcx+8等于8,
因为内存地址8上面不存在任何内容, 这条指令就会执行失败.
因为这条指令已经带有检查null的作用, 所以第二份代码中你看不到像第一份代码中的cmp指令.

熟悉c语言的可能会问, 这样的指令执行失败以后程序不会立刻退出吗?
答案是会, 如果你不做特殊的处理, 访问((MyClass*)NULL)->MyMember会导致程序立刻退出.
那么在CoreCLR中是如何处理的?

指令执行失败以后

CPU指令执行失败以后(内存访问失败, 除0等)时, 会传递一个硬件例外给内核, 然后内核会结束对应的进程.
但在结束之前它会允许进程补救, 补救的方法Windows和Linux都不一样.

在Linux上可以通过捕捉SIGSEGV处理内存访问失败, 示例代码如下

#include <signal.h>#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>jmp_buf recover_point;

static void sigsegv_handler(int sig, siginfo_t* si, void* unused) {  

 fprintf(stderr, "catched sigsegv\n");longjmp(recover_point, 1); }
 int main() {  
  struct sigaction action;action.sa_handler = NULL;action.sa_sigaction = sigsegv_handler;action.sa_flags = SA_SIGINFO;sigemptyset(&action.sa_mask);  
    if (sigaction(SIGSEGV, &action, NULL) != 0) {perror("bind signal handler failed");  
          abort();}    if (setjmp(recover_point) == 0) {      
       int* ptr = NULL;*ptr = 1;} else {        printf("recover success\n");;}    return 0; }

而在Windows上可以通过注册VectoredExceptionHandler处理硬件异常, 示例代码如下

#include "stdafx.h"
#include <Windows.h>
#include <setjmp.h>

void* gVectoredExceptionHandler = NULL; jmp_buf gRecoverPoint;

LONG WINAPI MyVectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo){  

 if (pExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_ACCESS_VIOLATION){        fprintf(stderr, "catched access violation\n");longjmp(gRecoverPoint, 1);}    return EXCEPTION_CONTINUE_SEARCH; }int main(){gVectoredExceptionHandler = AddVectoredExceptionHandler(TRUE, (PVECTORED_EXCEPTION_HANDLER)MyVectoredExceptionHandler);
   if (setjmp(gRecoverPoint) == 0){        int* ptr = NULL;*ptr = 1;}    else{        printf("recover success\n");}    return 0; }

在上面的代码中我使用了longjmp来从异常中恢复, 这是最简单的做法但也会带来很多问题, 接下来我们看看CoreCLR会如何处理这些异常.

CoreCLR中的处理 (Linux, OSX)

我们先来看Linux上CoreCLR是如何处理的, 以下代码来源于CoreCLR 1.1.0, OSX上的处理逻辑和Linux一样.

首先CoreCLR会注册SIGSEGV的处理器, 在pal\src\exception\signal.cpp中可以找到以下的代码

BOOL SEHInitializeSignals(DWORD flags){TRACE("Initializing signal handlers\n");   
 /* we call handle_signal for every possible signal, even  
      if we don't provide a signal handler.
   
   handle_signal will set SA_RESTART flag for specified signal.
 
     Therefore, all signals will have SA_RESTART flag set, preventing
   
     slow Unix system calls from being interrupted. On systems without
 
      siginfo_t, SIGKILL and SIGSTOP can't be restarted, so we don't
 
      handle those signals. Both the Darwin and FreeBSD man pages say
   
      that SIGKILL and SIGSTOP can't be handled, but FreeBSD allows us
 
      to register a handler for them anyway. We don't do that.
   
      see sigaction man page for more details
      */handle_signal(SIGILL, sigill_handler, &g_previous_sigill); handle_signal(SIGTRAP, sigtrap_handler, &g_previous_sigtrap);handle_signal(SIGFPE, sigfpe_handler, &g_previous_sigfpe);handle_signal(SIGBUS, sigbus_handler, &g_previous_sigbus);handle_signal(SIGSEGV, sigsegv_handler, &g_previous_sigsegv);handle_signal(SIGINT, sigint_handler, &g_previous_sigint);handle_signal(SIGQUIT, sigquit_handler, &g_previous_sigquit);

这里除了注册SIGSEGV以外还会注册其他信号的处理器, 接下来看sigsegv_handler的内容:

common_signal_handler的内容:

继续追下去会很长, 这里就只贴跟踪的调用流程了:


  跳过去以后会继续处理, 不再返回

总结:

  • 在Linux上

    • 这是为了可以和Windows共享处理的代码

    • 如果对象是null并且访问对象的函数或者成员, 会触发SIGSEGV信号

    • CoreCLR捕捉到SIGSEGV信号后会根据信号生成类似Windows形式的EXCEPTION_POINTERS结构体

    • 处理例外时, 根据例外代码(0xC0000005L)转换为CLR中的NullReferenceException的对象

    • 回滚堆栈和调用finally中的代码

    • 跳到对应的处理例外(catch)的代码

例外处理不是这一篇的重点所以这里我就不详细解释了(目前还未弄清楚).

CoreCLR中的处理 (Windows)

在Windows上CoreCLR会注册一个VectoredHandler用于处理硬件例外:

这是vm\excep.cpp中的CLRAddVectoredHandlers函数, 启动时会调用

void CLRAddVectoredHandlers(void){#ifndef FEATURE_PAL// We now install a vectored exception handler on all supporting Windows architectures.g_hVectoredExceptionHandler = AddVectoredExceptionHandler(TRUE, (PVECTORED_EXCEPTION_HANDLER)CLRVectoredExceptionHandlerShim);    if (g_hVectoredExceptionHandler == NULL){LOG((LF_EH, LL_INFO100, "CLRAddVectoredHandlers: AddVectoredExceptionHandler() failed\n"));COMPlusThrowHR(E_FAIL);}LOG((LF_EH, LL_INFO100, "CLRAddVectoredHandlers: AddVectoredExceptionHandler() succeeded\n"));#endif // !FEATURE_PAL}

当硬件异常发生时会调用这个处理器, 代码同样在vm\excep.cpp,同样的, 继续跟下去会非常长我就只贴跟踪流程了:

总结:

  • 在Windows上

    • 如果对象是null并且访问对象的函数或者成员, 会触发硬件异常

    • CoreCLR通过CLRVectoredExceptionHandlerShim捕捉到异常

    • 调用原生的RaiseException抛出例外给ProcessCLRException处理

    • 处理例外时, 根据例外代码(0xC0000005L)转换为CLR中的NullReferenceException的对象

    • 回滚堆栈和调用finally中的代码

    • 跳到对应的处理例外(catch)的代码

特殊情况的null检查

注意到上面第二份代码中的访问异常是在访问了0x8的时候出现的吗?
想想如果成员在更后面的位置, 例如0x10000, 并且在0x10000有内容存在的时候还可以检测出来吗?
这里我模拟一下特殊情况下的null检查, 看看CoreCLR是否可以正确处理.

测试使用的代码:

运行时的汇编代码:

注意图中红框的部分, CoreCLR加了额外的cmp, 成功避过了使用VirtualAlloc设下的陷阱.

你也可能会问, 如果使用VirtualAlloc来在0x8分配内存可以骗过CoreCLR吗?
事实上VirtualAlloc不能在0x8分配内存, 可以分配到的虚拟内存地址有范围限制,
如果成员的位置大于最小可以分配的虚拟内存地址, 则CoreCLR会插入一个额外的检查, 所以这种情况是骗不过CoreCLR的.

性能测试

我们再来测下自动抛出NullReferenceException和手动抛出NullReferenceException性能有多大的差别

测试的代码如下:

public static string GetString(){  
 return null; }public static void BenchmarkNullReferenceException(){  
   for (int x = 0; x < 100000; ++x){        try{          
              string str = GetString();  
            int length = str.Length;}        
      catch (Exception ex){}} }
   public static void BenchmarkManualNullReferenceException() {  
        for (int x = 0; x < 100000; ++x){      
      try{          
        string str = GetString();        
         if (str == null){              
            throw new NullReferenceException();}          
         int length = str.Length;}      
      catch (Exception ex){}} }

测试结果:

BenchmarkNullReferenceException: 0.9024312s
BenchmarkManualNullReferenceException: 0.9746265s

测试的结果比较出乎意料,
BenchmarkNullReferenceException和BenchmarkManualNullReferenceException在Debug和Release配置下所花的时间都是1秒左右,
这也说明了处理硬件异常的消耗相对于处理CLR异常的消耗并不大, 甚至还比手动抛出的消耗更小.

为什么要这样实现null检查

最常见也是最容易理解的null检查可能是在底层生成类似test rcx, rcx; jne 1f; call ThrowNullReferenceException; 1:的代码,
然而CoreCLR并不采用这种办法, 我个人推测有这些原因:

  • 可以节省生成的代码大小, 一条检查用的cmp指令只占2个字节

  • 可以提升检查性能, 例如访问成员时直接使用mov 寄存器, [对象寄存器+成员偏移值]即可同时取出值并检查是否null, 不需要额外的检查指令

  • 可以捕捉非托管代码中的异常, 调用使用c写的代码中发生了内存访问错误也可以捕捉到

参考链接

这篇文章参考了以下链接, 并且还在github上向CoreCLR提过了相关问题

  • https://www.codeproject.com/kb/cpp/exceptionhandler.aspx?fid=3666&df=90&mpp=25&noise=3&sort=position&view=quick&fr=51

  • https://stackoverflow.com/questions/27926085/how-can-i-register-a-structured-exception-handler-in-assembly-on-x86-64-architec

  • http://www.osronline.com/article.cfm?article=469

  • http://man7.org/linux/man-pages/man7/signal.7.html

  • https://msdn.microsoft.com/en-us/library/ms254246(v=vs.80).aspx

  • https://msdn.microsoft.com/en-us/library/windows/desktop/ms679331(v=vs.85).aspx

  • https://msdn.microsoft.com/en-us/library/windows/desktop/ms681419(v=vs.85).aspx

  • https://msdn.microsoft.com/en-us/library/windows/desktop/ms679274(v=vs.85).aspx

  • https://github.com/dotnet/coreclr/issues/11766

这篇相对来说比较易懂, 之前讲好的JIT篇要继续延期, 请大家耐心等待了.

相关文章:

  • 《代码的未来》读书笔记:内存管理与GC那点事儿

  • CoreCLR源码探索(一) Object是什么

  • CoreCLR源码探索(二) new是什么

  • CoreCLR源码探索(三) GC内存分配器的内部实现

  • .NET跨平台之旅:corehost 是如何加载 coreclr 的

  • .NET CoreCLR开发人员指南(上)

  • CoreCLR源码探索(四) GC内存收集器的内部实现 分析篇

  • CoreCLR源码探索(五) GC内存收集器的内部实现 调试篇

原文地址:http://www.cnblogs.com/zkweb/p/6898627.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/325420.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java中随机生成26个字母组合的随机验证码

实现原理&#xff0c;通过随机生成1-52的随机数&#xff0c;然后从定义好的字母数字中取元素进行组合。 下面是代码&#xff1a; public static void login(){String [] yan {"a","b","c","d","e","f","g&q…

详解 Java 中的三种代理模式

转载自 详解 Java 中的三种代理模式 代理模式 代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能. 这里使用到编程中的一个思想:不要随意去修改…

Ubuntu上配置SQL Server Always On Availability Group

下面简单介绍一下如何在Ubuntu上一步一步创建一个SQL Server AG&#xff08;Always On Availability Group&#xff09;&#xff0c;以及配置过程中遇到的坑的填充方法。 目前在Linux上可以搭建两种类型的SQL Server AG&#xff0c;一种是高可用性的结构同时使用Cluster服务器提…

架构必备「RESTful API」设计技巧经验总结

转载自 架构必备「RESTful API」设计技巧经验总结 【译者注】本文是作者在自己的工作经验中总结出来的RESTful API设计技巧&#xff0c;虽然部分技巧仍有争议&#xff0c;但总体来说还是有一定的参考价值的。以下是译文。 简单说一下代码重用 记得在Ken Rogers的Medium博客…

比较两个title是否相等(差点把我送走)

事情是这样的 把相同的分类找出来返回给前端我就比较了title 我用了两个比较字符串&#xff0c;结果vos里面输出的全是null 后来 想到字符串比较的是地址 结束 总结 忘记基础害人不浅

开篇有益-解析微软微服务架构eShopOnContainers(一)

为了推广.Net Core&#xff0c;微软为我们提供了一个开源Demo-eShopOnContainers&#xff0c;这是一个使用Net Core框架开发的&#xff0c;跨平台&#xff08;几乎涵盖了所有平台&#xff0c;windows、mac、linux、android、ios&#xff09;的&#xff0c;基于微服务架构的&…

React打包运行

项目打包运行 npm run build //生成打包文件 npm install -g serve //全局下载服务器包 serve build //通过服务器命令运行打包项目 访问: http://localhost:5000 //浏览器访问

端午将至……想和程序猿Coding个粽子行不行?

端午将至……都说程序猿无所不能……不造 Coding个粽子行不行&#xff1f; Build Tour 2017 世界巡回展已经开始倒计时……大家都报完名了么&#xff1f; 反正&#xff0c;M姐现在已经安排了我软的程序猿们不远万里的来到中国&#xff0c;他们将会在上海和北京两地&#xff0c;…

把本地文件上传到gitee

第一步 先克隆远程文件到本地 第二步 上传本地文件到远程

【活动】HoloLens 黑科技等你来探秘

微软全息眼镜&#xff08;HoloLens&#xff09;是微软最新推出的混合现实头显设备。酷炫的全息图像和包括语音控制和手势控制的全自然交互手段给用户带来前所未有的体验&#xff0c;目前已经在制造、医疗、建筑、娱乐等多个行业展现了非常好的应用前景和市场潜力。HoloLens既是…

ASP.NET Core MVC 模型绑定用法及原理

前言 查询了一下关于 MVC 中的模型绑定&#xff0c;大部分都是关于如何使用的&#xff0c;以及模型绑定过程中的一些用法和概念&#xff0c;很少有关于模型绑定的内部机制实现的文章&#xff0c;本文就来讲解一下在 ASP.NET Core MVC 中模型绑定是如何实现的&#xff0c;以及它…

IDEA无法加载log文件

如图所示&#xff0c;无论怎么生成log文件&#xff0c;idea文件列表始终不显式 解决方法 打开setting 打开File Types 选择文本文档Text&#xff0c;添加后缀*.log

使用sqlserver搭建高可用双机热备的Quartz集群部署【附源码】

一般拿Timer和Quartz相比较的&#xff0c;简直就是对Quartz的侮辱&#xff0c;两者的功能根本就不在一个层级上&#xff0c;如本篇介绍的Quartz强大的集群机制&#xff0c;可以采用基于 sqlserver&#xff0c;mysql的集群方案&#xff0c;当然还可以在第三方插件的基础上实现q…

一个正则表达式酿成的惨案

转载自 一个正则表达式酿成的惨案 导读&#xff1a;正则表达式是程序员经常使用的工具之一。本文作者通过一个正则表达式的陷阱&#xff0c;先深入剖析了出现问题的原因&#xff0c;后给出怎么处理这类问题的方法。最后还给出了一些检测常见正则表达式问题的工具&#xff0c…

详解C# Tuple VS ValueTuple(元组类 VS 值元组)

C# 7.0已经出来一段时间了&#xff0c;大家都知道新特性里面有个对元组的优化&#xff1a;ValueTuple。这里利用详尽的例子详解Tuple VS ValueTuple&#xff08;元组类VS值元组&#xff09;&#xff0c;10分钟让你更了解ValueTuple的好处和用法。 如果您对Tuple足够了解&#…

Eclipse把默认为Gbk的编码变为UTF-8

菜单栏Windows–>Preferences,左侧导航栏展开General–>Workspace,修改左下角的Text file encoding&#xff0c;选中Other改为UTF-8即可

从LINQ开始之LINQ to Objects(上)

LINQ概述 LINQ&#xff0c;语言集成查询(Language Integrated Query)&#xff0c;它允许使用C#或VB代码以查询数据库相同的方式来操作不同的数据源。 1.LINQ体系结构 从上图可以看出&#xff0c;LINQ总共包括五个部分&#xff1a;LINQ to Objects、LINQ to DataSets、LINQ to …