举例说明
- 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 对象,很显然这段代码会抛异常,那为什么会抛异常呢?要想找原因,需要从最底层的汇编研究起。
可以使用 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 区
冲出去。。。哈哈。
有了这个想法之后,我决定在 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 不得不生成代码来判断。
文章来源于【dotnet编程大全】
该文章知识作为个人笔记,大部分知识来源于书本或网络整理总结;