一:背景

1. 一个有趣的话题

最近在看 硬件异常 相关知识,发现一个有意思的空引用异常问题,拿出来和大家分享一下,为了方便讲述,先上一段有问题的代码。


namespace ConsoleApp2
{
internal class Program
{
static Person person = null; static void Main(string[] args)
{
var age = person.age; Console.WriteLine(age);
}
} public class Person
{
public int age;
}
}

由于 person 是一个 null 对象,很显然这段代码会抛异常,那为什么会抛异常呢? 要想找原因,需要从最底层的汇编研究起。

二:异常原理分析

1. 从汇编上寻找答案

可以使用 Visual Studio 2022 的反汇编窗口,观察 var age = person.age; 处到底生成了什么。


---------------- var age = person.age; ---------------- 081D6154 mov ecx,dword ptr ds:[4C41F4Ch]
081D615A mov ecx,dword ptr [ecx+4]
081D615D mov dword ptr [ebp-3Ch],ecx

这三句汇编还是很好理解的,4C41F4Ch 存放的是 person 对象, ecx+4 是取 person.age,最后一句就是将 age 放在 ebp-3Ch 栈位置上,接下来我们来看下 null 时的 ecx 到底是多少,截图如下:

从图中可以看到,此时的 ecx=0000000,如果大家了解 windows 的虚拟内存布局,应该知道在虚拟内存的 0~0x0000ffff 范围内是属于 null 禁入区,凡是落在这个区一概属访问违例,画个图就像下面这样。

到这里原理就搞清楚了,因为 [ecx+4] = [4] 是落在这个 null 区所致, 但是。。。。 大家有没有发现一个问题,对,就是这里的 [ecx+4],因为这里有一个 +4 偏移来取 age 字段,那我能不能在 person 中多定义一些字段,然后取最后一个字段从而从 null 区 冲出去。。。哈哈。

2. 真的可以冲出 null 区吗

有了这个想法之后,我决定在 Person 类中定义 10w 个 age 字段,参考代码如下:


namespace ConsoleApp2
{
internal class Program
{
static Person person = null; static void Main(string[] args)
{
var str = @"public class Person
{
{0}
}"; var lines = Enumerable.Range(0, 100000).Select(m => $"public int age{m};"); var fields = string.Join("\n", lines); var txt = str.Replace("{0}", fields); File.WriteAllText("Person.cs", txt); Console.WriteLine("person.cs 生成完毕");
}
}
}

代码执行后,Person.cs 就会如期生成,接下来读取 person.age99999 看看有没有奇迹发生,参考代码如下:


internal class Program
{
static Person person = null; static void Main(string[] args)
{
var age = person.age99999; Console.WriteLine(age);
}
}

我去,万万没想到,把 ClassLoader 给弄崩了。。。。 得,那只能改 20000 个 age 试试看吧,参考代码如下:


internal class Program
{
static Person person = null; static void Main(string[] args)
{
var age = person.age19999; Console.WriteLine(age);
}
}

接下来我们将断点放在 var age = person.age19999; 上继续看反汇编代码。


------------- var age = person.age19999; -------------
0804657E mov ecx,dword ptr ds:[49F1F4Ch]
08046584 mov dword ptr [ebp-40h],ecx
08046587 mov ecx,dword ptr [ebp-40h]
0804658A cmp dword ptr [ecx],ecx
0804658C mov ecx,dword ptr [ebp-40h]
0804658F mov ecx,dword ptr [ecx+13880h]
08046595 mov dword ptr [ebp-3Ch],ecx

从上面的汇编代码可以看出几点信息。

  • 汇编代码行数多了。

  • ecx+13880h 冲出了 null 区(FFFF) 的边界。

接下来单步调试汇编,发现在 cmp dword ptr [ecx],ecx 处抛了异常。。。

大家都知道此时的 ecx 的地址是 0 ,从 ecx 上取内容肯定会抛访问违例,而且这段代码很诡异,一般来说 cmp 之后都是类似 jz,jnz 跳转指令,而它仅仅是个半残之句。。。

从这些特征看,这是 JIT 故意在取偏移之前尝试判断 ecx 是不是 null,动机不纯哈。。。。

三:总结

从这些分析中可以得知,JIT 还是很智能的。

  • 当偏移值落在 0~FFFF 禁入区内,JIT 就不生成判断代码来减少代码体积。

  • 在偏移值冲出了 0~FFFF 禁入区,JIT 不得不生成代码来判断。

哈哈,本篇是不是很有意思,希望对大家有帮助。

为什么 C# 访问 null 字段会抛异常?的更多相关文章

  1. 扩展方法where方法查询不到数据,不会抛异常,也不是返回的null

    如题,“扩展方法where方法查询不到数据,不会抛异常,也不是返回的null”,示例代码如下: Product类: public class Product { private string name ...

  2. CloudStack的VO在调用setRemoved方法抛异常的原因

    今天在开发中发现一个问题,本来想对一个VO对象的removed值赋值,然后去update一下这条记录,一个最简单的set方法,但是在调用时直接抛异常了. 1: public void setRemov ...

  3. poco json 中文字符,抛异常JSON Exception -->iconv 转换 备忘录。

    起因 最近linux服务器通信需要用到json. jsoncpp比较出名,但poco 1.5版本以后已经带有json库,所以决定使用poco::json(linux 上已经用到了poco这一套框架). ...

  4. iOS开发——网络篇——UIWebview基本使用,NSInvocation(封装类),NSMethodSignature(签名),JavaScript,抛异常,消除警告

    一.UIWebView简介 1.UIWebView什么是UIWebViewUIWebView是iOS内置的浏览器控件系统自带的Safari浏览器就是通过UIWebView实现的 UIWebView不但 ...

  5. C++11 不抛异常的new operator

    在google cpp style guide里面明确指出:we don't use exceptions C++11的noexcept关键字为这种选择提供了便利. C++11以前,提及malloc和 ...

  6. C#在父窗口中调用子窗口的过程(无法访问已释放的对象)异常,不存在从对象类型System.Windows.Forms.DateTimePicker到已知的托管提供程序本机类型的映射。

    一:C#在父窗口中调用子窗口的过程(无法访问已释放的对象)异常 其实,这个问题与C#的垃圾回收有关.垃圾回收器管 理所有的托管对象,所有需要托管数据的.NET语言(包括 C#)都受运行库的 垃圾回收器 ...

  7. java通过抛异常来返回提示信息

    结论:如果把通过抛异常的方式得到提示信息,可以使用java.lang.Throwable中的构造函数: public Throwable(String message) { fillInStackTr ...

  8. java反射机制(访问私有字段和私有方法)

    来自:http://tutorials.jenkov.com/java-reflection/private-fields-and-methods.html 尽管我们通常认为通过JAVA的反射机制来访 ...

  9. 考虑实现一个不抛异常的swap

    Effective C++:参考自harttle land 类的swap实现与STL容器是一致的:提供swap成员函数, 并特化std::swap来调用那个成员函数. class Widget { p ...

随机推荐

  1. windows10家庭版启用组策略gpedit.msc

    启用组策略gpedit.msc 家庭版很多功能不能使用,凑巧用的就是家庭版. 还想使用gpedit.msc来关闭windows10的更新. 找到一个可行的方法. 需要创建一个脚本. 如果你没有编辑器, ...

  2. 2021牛客暑期多校训练营3 J 思维

    传送门 J-Counting Triangles_2021牛客暑期多校训练营3 (nowcoder.com) 题目 Goodeat finds an undirected complete graph ...

  3. Hyperledger Fabric 部署在多个主机上

    前言 在实验Hyperledger Fabric无排序组织以Raft协议启动多个Orderer服务.TLS组织运行维护Orderer服务中,我们已经完成了使用提供 TLS-CA 服务的 council ...

  4. mouseenter 和 mouseover 的区别

    当鼠标移动到元素上时就会触发mouseenter事件 类似mouseover,它们两者之间的差别是 mouseover鼠标经过自身盒子会触发,经过子盒子还会触发.mouseenter只会经过自身盒子触 ...

  5. 《手写Mybatis》第5章:数据源的解析、创建和使用

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 管你吃几碗粉,有流量就行! 现在我们每天所接收的信息量越来越多,但很多的个人却没有多 ...

  6. netty系列之:netty中的核心MessageToByte编码器

    目录 简介 MessageToByte框架简介 MessageToByteEncoder ByteToMessageDecoder ByteToMessageCodec 总结 简介 之前的文章中,我们 ...

  7. 聊聊Lock接口的lock()和lockInterruptible()有什么区别?

    lock()和lockInterruptible()都表示获取锁,唯一区别是,当A线程调用lock()或lockInterruptible()方法获取锁没有成功而进入等待锁的状态时,若接着调用该A线程 ...

  8. Python 树表查找_千树万树梨花开,忽如一夜春风来(二叉排序树、平衡二叉树)

    什么是树表查询? 借助具有特殊性质的树数据结构进行关键字查找. 本文所涉及到的特殊结构性质的树包括: 二叉排序树. 平衡二叉树. 使用上述树结构存储数据时,因其本身对结点之间的关系以及顺序有特殊要求, ...

  9. 编写引入svg

    SVG是一种XML语言,类似XHTML,可以用来绘制矢量图形,例如右面展示的图形.SVG可以通过定义必要的线和形状来创建一个图形,也可以修改已有的位图,或者将这两种方式结合起来创建图形.图形和其组成部 ...

  10. QMetaObject::connectSlotsByName: No matching signal for XXX 原理探究

    问题引出: 在尝试实现<Qt5.9 c++开发指南>混合UI编程章节时,用纯代码形式实现了个小按钮,然后加了个对应的槽函数,运行时就提示了这个信息. 原因探究: 首先查阅官方手册中的说明: ...