一:背景

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. route -n 讲解

    我们经常会出现临时添加路由,或者是路由重启路由丢失等导致网络不通的情况,上网查发现很多介绍或者没有实验跟进导致理解的时候很费劲的情况,可能人家认为是比较简单的事情了,但是很多不尽然,老手也不一定能很快 ...

  2. Codeforces Round #754 (Div. 2), problem: (A) A.M. Deviation泪目 万万没想到狂wa是因为这

    Problem - A - Codeforces 题目 题意很简单每次操作可以使得a1 a2  a3任意两个数分别+1  -1 求最后使得a+c-2b绝对值的最小值 BUG就是最后忽略了-2和2这一点 ...

  3. mysql的半同步复制

    1. binlog dump线程何时向从库发送binlog mysql在server层进行了组提交之后,为了提高并行度,将提交阶段分为了 flush sync commit三个阶段,根据sync_bi ...

  4. events.js 源码分析

    events.js 源码分析 1. 初始化 // 使用 this.ee = new EventEmitter(); // 源码 // 绑定this域,初始化 _events,_eventsCount和 ...

  5. 共读《redis设计与实现》-数据结构篇

    准备将之前攒下的书先看一遍,主要是有个大概的了解,以后用的时候也知道在哪里找.所以准备开几篇共读的帖子,激励自己多看一些书. Redis 基于 简单动态字符串(SDS).双端链表.字典.压缩列表.整数 ...

  6. 移动端屏幕适配(rem+js)

    什么是移动端适配 在制作webapp时,一个很关键的问题就是适配各种机型不同屏幕的大小,让每种机型上的布局看起来都尽量一样. 也就是说用同一套代码在不同分辨率的手机上跑时,页面元素间的间距.留白,以及 ...

  7. docker:registry

    存放docker镜像(mage)的地址,可供人上传下载镜像包: 下载 docker search whalesay --搜索whalesay镜像,该镜像用命令行的形式画了个鲸鱼并说了句话 docker ...

  8. 引入『客户端缓存』,Redis6算是把缓存玩明白了…

    原创:微信公众号 码农参上,欢迎分享,转载请保留出处. 哈喽大家好啊,我是没更新就是在家忙着带娃的Hydra. 在前面介绍两级缓存的文章中,我们总共给出了4种实现方案,在项目中整合了本地缓存Caffe ...

  9. Next.js 在 Serverless 中从踩坑到破茧重生

    作者 杨苏博,偏后端的全栈开发,目前负责腾云扣钉的 Cloud Studio 产品.在团队中负责接技术架构设计与 Review.Cloud Studio 编辑器内核设计与开发.部分核心插件设计与开发: ...

  10. node技术是啥?

    node.js 一句话,就是把js代码放在服务器段运行的一种技术.