• C# 访问 null 字段会抛异常原因探究


    举例说明 

    1. namespace ConsoleApp2
    2. {
    3.     internal class Program
    4.     {
    5.         static Person person = null;
    6.         static void Main(string[] args)
    7.         {
    8.             var age = person.age;
    9.             Console.WriteLine(age);
    10.         }
    11.     }
    12.     public class Person
    13.     {
    14.         public int age;
    15.     }
    16. }

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

    二:异常原理分析

    1. 从汇编上寻找答案

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

    1. ----------------  var age = person.age;   ----------------
    2. 081D6154  mov         ecx,dword ptr ds:[4C41F4Ch]  
    3. 081D615A  mov         ecx,dword ptr [ecx+4]  
    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 字段,参考代码如下:

    1. namespace ConsoleApp2
    2. {
    3.     internal class Program
    4.     {
    5.         static Person person = null;
    6.         static void Main(string[] args)
    7.         {
    8.             var str = @"public class Person
    9.                         {
    10.                             {0}
    11.                         }";
    12.             var lines = Enumerable.Range(0100000).Select(m => $"public int age{m};");
    13.             var fields = string.Join("\n", lines);
    14.             var txt = str.Replace("{0}", fields);
    15.             File.WriteAllText("Person.cs", txt);
    16.             Console.WriteLine("person.cs 生成完毕");
    17.         }
    18.     }
    19. }

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

    1.     internal class Program
    2.     {
    3.         static Person person = null;
    4.         static void Main(string[] args)
    5.         {
    6.             var age = person.age99999;
    7.             Console.WriteLine(age);
    8.         }
    9.     }

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

    1.     internal class Program
    2.     {
    3.         static Person person = null;
    4.         static void Main(string[] args)
    5.         {
    6.             var age = person.age19999;
    7.             Console.WriteLine(age);
    8.         }
    9.     }

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

    1. ------------- var age = person.age19999;  -------------
    2. 0804657E  mov         ecx,dword ptr ds:[49F1F4Ch]  
    3. 08046584  mov         dword ptr [ebp-40h],ecx  
    4. 08046587  mov         ecx,dword ptr [ebp-40h]  
    5. 0804658A  cmp         dword ptr [ecx],ecx  
    6. 0804658C  mov         ecx,dword ptr [ebp-40h]  
    7. 0804658F  mov         ecx,dword ptr [ecx+13880h]  
    8. 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 不得不生成代码来判断。

    文章来源于【dotnet编程大全】


    该文章知识作为个人笔记,大部分知识来源于书本或网络整理总结;

     

  • 相关阅读:
    2022年小美赛“认证杯”数学建模ABCD题初步分析&选题建议
    马尔可夫预测案例分析
    SVG 渐变边框在 CSS 中的应用
    ubuntu apt安装的软件克隆
    基于SpringBoot的学生班级考勤管理系统
    优秀的 Verilog/FPGA开源项目介绍(三十五)- TinyML
    博士生做科研想 idea 发现早就有人做过了,该怎么调整心态?
    【CentOS 7】克隆虚拟机
    Python【多分支练习】
    Java项目:JSP旅游产品销售管理系统
  • 原文地址:https://blog.csdn.net/weixin_67336587/article/details/125534894