我们先来看看什么样的代码可以导致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
.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会如何处理这些异常.
如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!
加入交流群
请使用微信扫一扫!