<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          為什么 C# 訪問 null 字段會拋異常?

          共 6332字,需瀏覽 13分鐘

           ·

          2022-07-22 13:53

          一:背景

          1. 一個有趣的話題

          最近在看 硬件異常 相關知識,發(fā)現一個有意思的空引用異常問題,拿出來和大家分享一下,為了方便講述,先上一段有問題的代碼。


          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 禁入區(qū),凡是落在這個區(qū)一概屬訪問違例,畫個圖就像下面這樣。

          到這里原理就搞清楚了,因為 [ecx+4] = [4] 是落在這個 null 區(qū)所致, 但是。。。。 大家有沒有發(fā)現一個問題,對,就是這里的 [ecx+4],因為這里有一個 +4 偏移來取 age 字段,那我能不能在 person 中多定義一些字段,然后取最后一個字段從而從 null 區(qū) 沖出去。。。哈哈。

          2. 真的可以沖出 null 區(qū)嗎

          有了這個想法之后,我決定在 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(0100000).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 生成完畢");
                  }
              }
          }

          代碼執(zhí)行后,Person.cs 就會如期生成,接下來讀取 person.age99999 看看有沒有奇跡發(fā)生,參考代碼如下:


              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; 上繼續(xù)看反匯編代碼。


          ------------- 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 區(qū)(FFFF) 的邊界。

          接下來單步調試匯編,發(fā)現在 cmp dword ptr [ecx],ecx 處拋了異常。。。

          大家都知道此時的 ecx 的地址是 0 ,從 ecx 上取內容肯定會拋訪問違例,而且這段代碼很詭異,一般來說 cmp 之后都是類似 jz,jnz 跳轉指令,而它僅僅是個半殘之句。。。

          從這些特征看,這是 JIT 故意在取偏移之前嘗試判斷 ecx 是不是 null,動機不純哈。。。。

          三:總結

          從這些分析中可以得知,JIT 還是很智能的。

          • 當偏移值落在 0~FFFF 禁入區(qū)內,JIT 就不生成判斷代碼來減少代碼體積。

          • 在偏移值沖出了 0~FFFF 禁入區(qū),JIT 不得不生成代碼來判斷。

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


          瀏覽 29
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲第一综合 | 久久婷婷丁香五月天 | 日本亚洲欧洲免费 | 亚洲女人在线观看 | 91精品国产欧美一区二区百度云 |