CLR的黑暗面——栈与引用对象

    几乎所有的书或文章都说,引用类型的对象是在托管堆里面的,而值类型的对象可以在栈里也可以在堆里(例如:装箱与拆箱),但是,今天写一个推翻引用类型的对象是在托管堆里面的情况:
    首先,在这里使用了大量的Emit和反射,因为不喜欢直接写IL汇编,所以看起来有点累,但是IL的知识是必须的,否则很难看明白这些代码是在干什么。
    其次,要理解为什么会这样,需要了解一些程序底层的知识。
    最后,准确的说,这里的对象并不是一个真实的对象,仅仅是把某段内存认为是某个对象,一般的高级语言不允许这样的代码,所以只能用最基础的IL来做。
 1        static void Foo()
 2        {
 3            AssemblyName name = new AssemblyName("a");
 4            AssemblyBuilder a = AppDomain.CurrentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run);
 5            ModuleBuilder m = a.DefineDynamicModule("test"false);
 6            TypeBuilder tb = Bar(m);
 7            Type t = tb.CreateType();
 8            MethodInfo mi = t.GetMethod("test", BindingFlags.Public | BindingFlags.Static);
 9            mi.Invoke(nullnull);
10        }

11
12        static TypeBuilder Bar(ModuleBuilder m)
13        {
14            // 创建一个Foo类
15            TypeBuilder result = m.DefineType("Foo");
16            // 定义一个bar字段
17            FieldBuilder field = result.DefineField("bar"typeof(int), FieldAttributes.Assembly);
18            // Foo的实例构造器
19            ConstructorBuilder ctor = result.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes);
20            ILGenerator gen = ctor.GetILGenerator();
21            gen.Emit(OpCodes.Ldarg_0);
22            gen.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
23            gen.Emit(OpCodes.Ldarg_0);
24            gen.Emit(OpCodes.Ldc_I4_0);
25            gen.Emit(OpCodes.Stfld, field);
26            gen.Emit(OpCodes.Ret);
27
28            // 定义test方法
29            MethodBuilder method = result.DefineMethod("test",
30                MethodAttributes.Public | MethodAttributes.Static,
31                CallingConventions.Standard, typeof(void), Type.EmptyTypes);
32
33            gen = method.GetILGenerator();
34            // 定义本地变量1、2、3
35            LocalBuilder local1 = gen.DeclareLocal(typeof(int));
36            LocalBuilder local2 = gen.DeclareLocal(typeof(int));
37            LocalBuilder local3 = gen.DeclareLocal(typeof(int));
38
39            // 用local2的地址作为对象地址,调用实例构造器
40            gen.Emit(OpCodes.Ldloca, local2);
41            gen.Emit(OpCodes.Call, ctor);
42            // 用local2的地址作为对象地址,把数字123放到字段里面
43            gen.Emit(OpCodes.Ldloca, local2);
44            gen.Emit(OpCodes.Ldc_I4, 123);
45            gen.Emit(OpCodes.Stfld, field);
46
47            // 来看看local1里面是什么
48            gen.Emit(OpCodes.Ldstr, "local1 address=");
49            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("Write"new Type[] typeof(string) }));
50            gen.Emit(OpCodes.Ldloca, local1);
51            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine"new Type[] typeof(int) }));
52            gen.Emit(OpCodes.Ldstr, "local1=");
53            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("Write"new Type[] typeof(string) }));
54            gen.Emit(OpCodes.Ldloc, local1);
55            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine"new Type[] typeof(int) }));
56            // 来看看local2里面是什么
57            gen.Emit(OpCodes.Ldstr, "local2 address=");
58            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("Write"new Type[] typeof(string) }));
59            gen.Emit(OpCodes.Ldloca, local2);
60            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine"new Type[] typeof(int) }));
61            gen.Emit(OpCodes.Ldstr, "local2=");
62            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("Write"new Type[] typeof(string) }));
63            gen.Emit(OpCodes.Ldloc, local2);
64            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine"new Type[] typeof(int) }));
65            // 来看看local3里面是什么
66            gen.Emit(OpCodes.Ldstr, "local3 address=");
67            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("Write"new Type[] typeof(string) }));
68            gen.Emit(OpCodes.Ldloca, local3);
69            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine"new Type[] typeof(int) }));
70            gen.Emit(OpCodes.Ldstr, "local3=");
71            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("Write"new Type[] typeof(string) }));
72            gen.Emit(OpCodes.Ldloc, local3);
73            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine"new Type[] typeof(int) }));
74
75            // 用local2的地址作为对象地址,看看字段里面是什么
76            gen.Emit(OpCodes.Ldstr, "Field  address=");
77            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("Write"new Type[] typeof(string) }));
78            gen.Emit(OpCodes.Ldloca, local2);
79            gen.Emit(OpCodes.Ldflda, field);
80            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine"new Type[] typeof(int) }));
81            gen.Emit(OpCodes.Ldstr, "Field =");
82            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("Write"new Type[] typeof(string) }));
83            gen.Emit(OpCodes.Ldloca, local2);
84            gen.Emit(OpCodes.Ldfld, field);
85            gen.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine"new Type[] typeof(int) }));
86
87            gen.Emit(OpCodes.Ret);
88
89            return result;
90        }

91

    乍一看,代码有些长,下面仔细分析一下:
    1、Foo方法,作用是为Emit做准备,并且测试,调用"test"方法
    2、Bar方法,Emit一个叫"Foo"的引用类型的类,带有一个"Bar"的字段,并且有一个实例构造器和一个"test"方法
    3、在实例构造器中,对"Bar"字段赋初始值0
    4、在"test"方法中,用local2的地址作为对象的地址,Call实例构造器(注意不能用newobj),这样的效果等于初始化对象的初始值(也就是说,Call实例构造器后,"Bar"字段的值就是构造器中赋的初始值)
    5、再次用local2的地址作为对象的地址,给"Bar"字段赋数字123(注意,只有这里出现了123这个数字)
    6、从控制台分别输出local1、local2、local3,以及用local2的地址作为对象的地址的"Bar"字段的内存地址和他们的值
    代码的作用就是这些,看一下运行的结果:
local1 address=1240840
local1
=123
local2 address
=1240836
local2
=0
local3 address
=1240832
local3
=0
Field  address
=1240840
Field 
=123

    运行结果中,可以意外的发现,local1也是123,而且local1的地址和"Bar"字段的地址一致。
    当然这个值并不固定,而且local1、local2、local3的地址并不一定是递减的,也有可能是递增的,或者其它情况(虽然还没遇到,但是也不排除有分散的可能性)。
    在递减的情况下,local1与以local2的地址作为对象地址的"Bar"字段的地址是一致的,也就是说,以local2的地址作为对象地址的情况下,"Bar"字段并不是在堆上,而是在栈上。对local1的任何改动可以影响"Bar"字段,反过来,对"Bar"字段的任何改动也可以影响local1,因为本质上它们就是同一个地址。
    同样,以local2的地址作为对象地址的这个虚假引用类型的"对象",也是存在于栈上的,是从local2地址开始的,一片隶属于栈的内存。
    最后,需要注意的一点是,这个方法有种钻运行时漏洞的感觉,在x86 .net 2.0的环境上测试通过,不保证其它运行时也能正常运行。
原文地址:https://www.cnblogs.com/vwxyzh/p/1093782.html