为什么 C# 访问 null 字段会抛异常?
一:背景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; ----------------
081D6154mov ecx,dword ptr ds:
081D615Amov ecx,dword ptr
081D615Dmov dword ptr ,ecx这三句汇编还是很好理解的,4C41F4Ch 存放的是 person 对象, ecx+4 是取 person.age,最后一句就是将 age 放在 ebp-3Ch 栈位置上,接下来我们来看下 null 时的 ecx 到底是多少,截图如下:
https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c2e26d2b26b94979830d9999a25d5846~tplv-k3u1fbpfcp-zoom-1.image
从图中可以看到,此时的 ecx=0000000,如果大家了解 windows 的虚拟内存布局,应该知道在虚拟内存的 0~0x0000ffff 范围内是属于 null 禁入区,凡是落在这个区一概属访问违例,画个图就像下面这样。
https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fe87a4643bcc4e9a9488cb62f52eba66~tplv-k3u1fbpfcp-zoom-1.image
到这里原理就搞清楚了,因为 = 是落在这个 null 区所致, 但是。。。。 大家有没有发现一个问题,对,就是这里的 ,因为这里有一个 +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);
}
}https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/934f4b8a88aa4fdfaf8b8e33059b0e56~tplv-k3u1fbpfcp-zoom-1.image
我去,万万没想到,把 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;-------------
0804657Emov ecx,dword ptr ds:
08046584mov dword ptr ,ecx
08046587mov ecx,dword ptr
0804658Acmp dword ptr ,ecx
0804658Cmov ecx,dword ptr
0804658Fmov ecx,dword ptr
08046595mov dword ptr ,ecx从上面的汇编代码可以看出几点信息。
[*]汇编代码行数多了。
[*]ecx+13880h 冲出了 null 区(FFFF) 的边界。
接下来单步调试汇编,发现在 cmp dword ptr ,ecx 处抛了异常。。。
https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fcf797e3886e451da911d50726a15574~tplv-k3u1fbpfcp-zoom-1.image
大家都知道此时的 ecx 的地址是 0 ,从 ecx 上取内容肯定会抛访问违例,而且这段代码很诡异,一般来说 cmp 之后都是类似 jz,jnz 跳转指令,而它仅仅是个半残之句。。。
从这些特征看,这是 JIT 故意在取偏移之前尝试判断 ecx 是不是 null,动机不纯哈。。。。
三:总结
从这些分析中可以得知,JIT 还是很智能的。
[*]当偏移值落在 0~FFFF 禁入区内,JIT 就不生成判断代码来减少代码体积。
[*]在偏移值冲出了 0~FFFF 禁入区,JIT 不得不生成代码来判断。
哈哈,本篇是不是很有意思,希望对大家有帮助。
https://images.cnblogs.com/cnblogs_com/huangxincheng/345039/o_210929020104最新消息优惠促销公众号关注二维码.jpg
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]