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


风晓
风晓 2023-12-31 10:54:26 50302 赞同 0 反对 0
分类: 资源
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: nop
	IL_0001: ldnull
	IL_0002: stloc.0
	IL_0003: ldloc.0
	IL_0004: callvirt instance void ConsoleApp1.Program/MyClass::MyMethod()
	IL_0009: nop
	IL_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: nop
	IL_0001: ldnull
	IL_0002: stloc.0
	IL_0003: ldloc.0
	IL_0004: ldfld int32 ConsoleApp1.Program/MyClass::MyMember
	IL_0009: call void [System.Console]System.Console::WriteLine(int32)
	IL_000e: nop
	IL_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会如何处理这些异常.

如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!

评价 0 条
风晓L1
粉丝 1 资源 2038 + 关注 私信
最近热门资源
银河麒麟桌面操作系统备份用户数据  123
统信桌面专业版【全盘安装UOS系统】介绍  117
银河麒麟桌面操作系统安装佳能打印机驱动方法  109
银河麒麟桌面操作系统 V10-SP1用户密码修改  102
最近下载排行榜
银河麒麟桌面操作系统备份用户数据 0
统信桌面专业版【全盘安装UOS系统】介绍 0
银河麒麟桌面操作系统安装佳能打印机驱动方法 0
银河麒麟桌面操作系统 V10-SP1用户密码修改 0
作者收入月榜
1

prtyaa 收益393.62元

2

zlj141319 收益218元

3

1843880570 收益214.2元

4

IT-feng 收益209.03元

5

风晓 收益208.24元

6

777 收益172.71元

7

Fhawking 收益106.6元

8

信创来了 收益105.84元

9

克里斯蒂亚诺诺 收益91.08元

10

技术-小陈 收益79.5元

请使用微信扫码

加入交流群

请使用微信扫一扫!