8

为什么 C# 访问 null 字段会抛异常?

 2 years ago
source link: https://www.cnblogs.com/huangxincheng/p/16395467.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 到底是多少,截图如下:

c2e26d2b26b94979830d9999a25d5846~tplv-k3u1fbpfcp-zoom-1.image

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

fe87a4643bcc4e9a9488cb62f52eba66~tplv-k3u1fbpfcp-zoom-1.image

到这里原理就搞清楚了,因为 [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);
        }
    }

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;  -------------
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 处抛了异常。。。

fcf797e3886e451da911d50726a15574~tplv-k3u1fbpfcp-zoom-1.image

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

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

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

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

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

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

图片名称

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK