在MSDN上论坛看到下面一个代码:
demo.cs
1. using System; 2. 3. public class A 4. { 5. public static string strText; 6. 7. static A() 8. { 9. strText = "aaaaaa"; 10. } 11. } 12. 13. public class B : A 14. { 15. static B() 16. { 17. strText = "ssss"; 18. } 19. } 20. 21. public class Demo 22. { 23. public static void Main(string[] args) 24. { 25. B b = new B(); 26. A a = new A(); 27. 28. Console.WriteLine(B.strText); 29. } 30. } |
编译并执行以后,你会发现B的静态构造函数先被调用,然后是A的静态构造函数被调用。但是问题是,Console.WriteLine那一行竟然打印的是在B的静态构造函数里面设置的值:“ssss”。
我第一次看到这个问题的时候,也是非常惊讶,经过仔细重现以后,我判定是A的静态构造函数自动被B的静态构造函数所调用,即static B()的实际代码看起来应该类似下面这样:
static B()
{
A();
strText = "ssss";
}
嗯,看起来也没有什么特别嘛,跟类的构造函数是一样的,类的构造函数都会先调用基类的构造函数,然后再执行自己构造函数包含的代码。但是如果你把25行和26行对换的话,你会发现A的静态构造函数又会先于B的静态构造函数执行。
看起来,刚才所说的“基类的静态构造函数先于类型的静态构造执行”的论断有些问题……
那么实际情况呢?实际情况应该是--类的静态构造函数在类型第一次被引用的时候调用。好了,本来说到这里实际上是可以打住了,不过我们既然有些CLR的源代码(sscli 2.0),那看看源码里面到底怎么回事吧。
由于我是使用Windbg调试的sscli代码,把调试命令以及其结果全部贴出来的话,需要介绍很多背景知识,打算另写几篇文章讲解,这里就把关键的命令以及其输出结果贴出来,等背景知识讲完了以后,再把整个分析过程描述一下(工程量好像还是蛮大的)。
第一我可以负责任地说,针对demo.cs里面的这两段代码:
25. B b = new B();
26. A a = new A();
CLR的确是先即时编译( JIT)B的静态构造函数,然后再即时编译A的静态构造函数。
第二触发A的静态构造函数的编译是由执行B的静态构造函数的过程所引起的。
第三A 的静态构造函数即时编译完成以后,立即就被调用了,虽然这个时候B的静态构造函数只被执行了一半。
下面是static B()被最后JIT成的汇编代码,为了各位网友阅读方便,我在关键的地方加了一些注释描述代码的意思。
# 04e4fe60是static B最后被 JIT的代码在内存存放的地址,通过查看MethodDesc获取到。 # 至于如何获取MethodDesc以及MethodDesc是什么,呃……属于背景知识。 0:000> !u 04e4fe60 Normal JIT generated code B..cctor() Begin 04e4fe60, size 65 # 由于我设置参数COMPlus_JitHalt的值为.cctor,即通知CLR在JIT完成任何一个类型的静 # 态构造函数后,立即中断托管程序(当然还有CLR本身)的执行。 # # 所以CLR在JIT完成以后,特意加上了一个断点。 >>> 04e4fe60 cc int 3 # 下面的几段代码都是所有函数通用的初始化代码,比如需要多大的堆栈空间之类的。看 # 不懂没关系,如果想看懂的话,需要理解堆栈内存空间分配的方式,以及x86汇编的一# 些指令的意义。 04e4fe61 55 push ebp 04e4fe62 8bec mov ebp,esp 04e4fe64 56 push esi 04e4fe65 33f6 xor esi,esi 04e4fe67 56 push esi 04e4fe68 51 push ecx 04e4fe69 52 push edx 04e4fe6a b901000000 mov ecx,1 04e4fe6f 6a00 push 0 04e4fe71 e2fc loop 04e4fe6f # 下面的代码判断是否要进行Just My Code方面的支持,Just My Code的实现原理我 # 还不是特别清楚,因此,呃……我们可以跳过……呃,还是跳过吧。 04e4fe73 b8d42b3c00 mov eax,3C2BD4h 04e4fe78 8b00 mov eax,dword ptr [eax] 04e4fe7a 85c0 test eax,eax 04e4fe7c 0f8407000000 je 04e4fe89 04e4fe82 b860ad4579 mov eax,offset mscorwks!JIT_DbgIsJustMyCode (7945ad60) 04e4fe87 ffd0 call eax 04e4fe89 90 nop 04e4fe8a fc cld 04e4fe8b 90 nop 04e4fe8c 90 nop # 32D11FC是字符串“ssss”的内存地址,因为“ssss”是常量,所以它的地址总是在程序 # 启动的时候就已经分配好了,当然啦,如何看这个地址的内存,后面会讲到。 # 下面几段话就是把“ssss”的内存地址放到栈上,这里的栈,我指的是CLR是栈式机的 # 栈,因为后续的操作,也就是下面这条C#语句 # ------------------------------------------------------------------------------------------------------------------------ # strText = “ssss” # 会转化成下面这几条IL语句 # ldstr “ssss” # stsfld string A::strText #------------------------------------------------------------------------------------------------------------------------ # ldstr指令在执行之前,要求”ssss”的地址已经是在栈顶的。下面这段汇编指令就是干这 # 件事情的。 04e4fe8d b8fc112d03 mov eax,32D11FCh 04e4fe92 8b00 mov eax,dword ptr [eax] # 3C2F54是A的MethodTable地址,什么是MethodTable以及它是干什么用的,也是 #背景知识。这篇文章不讲—否则就太长了,现在只要知道,这段汇编指令是为了让 # mscorwks!JIT_InitClass这个函数找到static A所对应的MethodDesc而做的操作。 04e4fe94 b9542f3c00 mov ecx,3C2F54h 04e4fe99 50 push eax # 我们的学习 C++的时候,几乎 所有的书籍都会说,C++ 实例的构造函数是在实例的内存 # 被分配成功以后,才会调用 C++实例的构造函数。即下面的C++代码 : # TestClass *ptc = new TestClass; # 如果从C语言的角度来看,实际上是分两步完成的: # # TestClass *ptc = (TestClass *)malloc(sizeof(TestClass)); # ptc->TestClass(); // 调用构造函数 # # 这样做是因为避免构造函数初始化实例成员的时候,发生访问违规的情况(因为实例 # 的内存已经预先分配好了)。 # # 说了这么多,实际上就是为了说明C#的静态构造函数的调用顺序也是这样的,一个类型 # 的静态变量在进程内存中是只能有一份备份,但是内存还是要被分配的。所以上面的 # 两个汇编指令将分配的内存地址,传给下面的mscorwks!JIT_InitClass函数。 # # 为什么是mscorwks!JIT_InitClass,而不是static A这个函数呢? # 因为静态构造函数只能被调用一次, 这个函数的作用就是: # # 1. 先看静态构造函数是否已经JIT过,如果没有,则即时编译这个静态构造函数。 # 2. 然后判断这个静态构造函数是否已经被调用过了,这个判断是通过读写一个标志位做# 到的。 # 3. 如果没有被调用过,则调用它 04e4fe9a b880e04479 mov eax,offset mscorwks!JIT_InitClass (7944e080) 04e4fe9f ffd0 call eax # 32D0D3C就是CLR单独保存类型A的静态成员所分配的内存空间,当然啦,我现在说你 # 肯定不相信,一会我会演示查看这段内存的方法。 # # 注意,32D0D3C是A的静态成员地址,不是B的,因为上面的C#语句 # ------------------------------------------------------------------------------------------------------------------------ # strText = “ssss”
# 完整的写,应该是 A.strText = “ssss”; # 04e4fea1 b83c0d2d03 mov eax,32D0D3Ch 04e4fea6 50 push eax # 调用一个通用的函数STSFLD_REF_helper,将A.strText的值赋为”ssss” 04e4fea7 b850bfa179 mov eax,offset mscorejt!STSFLD_REF_helper (79a1bf50) 04e4feac ffd0 call eax # 下面的指令是标准的函数退出前的清理操作。 04e4feae 90 nop 04e4feaf 54 push esp 04e4feb0 55 push ebp 04e4feb1 b814000000 mov eax,14h 04e4feb6 50 push eax 04e4feb7 b8e091a179 mov eax,offset mscorejt!check_stack (79a191e0) 04e4febc ffd0 call eax 04e4febe 8b75fc mov esi,dword ptr [ebp-4] 04e4fec1 8be5 mov esp,ebp 04e4fec3 5d pop ebp 04e4fec4 c3 ret |
上面关于B 的静态构造函数生成的汇编代码已经解释完毕了,现在的问题是,我们是怎样到达这里的?那当然是看堆栈啦,由于堆栈的调用次序和显示的顺序刚好相反,所以下面的注释序号是反向的:
0:000> kp ChildEBP RetAddr WARNING: Frame IP not in any known module. Following frames may be wrong. # # 5. 调用刚刚即时编译好了的 类型B的静态构造函数 # 0020d0b8 79366025 0x4e4fe6a # # 4. CallDescrWorker应该是 JIT编译器在托管程序执行过程当中,介入编译尚未被JIT的函数的入口 # 然而遗憾的是,这几个函数,以及JIT如何介入的机制我还是不是特别熟悉,所以……呃…… # 你知道的……就这么着吧 # 0020d0c8 7937d2c2 mscorwks!CallDescrWorkerInternal+0x33 0020d4f4 7937d1b7 mscorwks!CallDescrWorker(void * pSrcEnd = 0x0020d670, unsigned int numStackSlots = 0, struct ArgumentRegisters * pArgumentRegisters = 0x0020d640, unsigned int fpRetSize = 0, void * pTarget = 0x003c31d0)+0xd2 [c:\sscli20\clr\src\vm\class.cpp @ 11285] 0020d620 794a76c1 mscorwks!CallDescrWorkerWithHandler(void * pSrcEnd = 0x0020d670, unsigned int numStackSlots = 0, struct ArgumentRegisters * pArgumentRegisters = 0x0020d640, unsigned int fpReturnSize = 0, void * pTarget = 0x003c31d0, int fCriticalCall = 0)+0x187 [c:\sscli20\clr\src\vm\class.cpp @ 11198] # # 3. 找到类型B的静态构造函数对应的MethodDesc,调用这个函数 # 0020d7d8 794a6fa6 mscorwks!MethodDesc::CallDescr(unsigned char * pTarget = 0x003c31d0 "???", class MetaSig * pMetaSigOrig = 0x0020d850, unsigned int64 * pArguments = 0x00000000, int fIsStatic = 1, int fCriticalCall = 0, int fPermitValueTypes = 0)+0x711 [c:\sscli20\clr\src\vm\method.cpp @ 1883] 0020d800 792f8104 mscorwks!MethodDesc::CallTargetWorker(unsigned char * pTarget = 0x003c31d0 "???", class MetaSig * pMetaSig = 0x0020d850, unsigned int64 * pArguments = 0x00000000, int fCriticalCall = 0, int fPermitValueTypes = 0)+0x46 [c:\sscli20\clr\src\vm\method.cpp @ 1572] 0020d820 79321a25 mscorwks!MethodDescCallSite::CallTargetWorker(unsigned int64 * pArguments = 0x00000000, int fPermitValueTypes = 0)+0x34 [c:\sscli20\clr\src\vm\method.hpp @ 1804] 0020d834 7949522d mscorwks!MethodDescCallSite::Call(unsigned int64 * pArguments = 0x00000000)+0x15 [c:\sscli20\clr\src\vm\method.hpp @ 1910] # # 2. 在类型B的方法表里面,找到它的静态构造函数,并且执行它。 # 0020d8d0 79494e1b mscorwks!MethodTable::RunClassInitWorker(class MethodDesc * pInitMethod = 0x003c3080, class OBJECTREF * pThrowable = 0x0020e054)+0xbd [c:\sscli20\clr\src\vm\methodtable.cpp @ 2692] 0020dab8 794955de mscorwks!MethodTable::RunClassInitEx(class OBJECTREF * pThrowable = 0x0020e054)+0x28b [c:\sscli20\clr\src\vm\methodtable.cpp @ 2657] 0020e4e8 79493859 mscorwks!MethodTable::DoRunClassInitThrowing(void)+0x38e [c:\sscli20\clr\src\vm\methodtable.cpp @ 2840] 0020e4fc 7955143f mscorwks!MethodTable::CheckRunClassInitThrowing(void)+0xa9 [c:\sscli20\clr\src\vm\methodtable.cpp @ 1902] 0020e5dc 79550d3e mscorwks!MethodDesc::DoPrestub(class MethodTable * pDispatchingMT = 0x00000000)+0x3ff [c:\sscli20\clr\src\vm\prestub.cpp @ 908] # # 1. 在前面的Main函数执行过程中,中途碰到需要即时编译类型B的静态构造函 # 数的要求,中断正常的托管程序执行顺序。由JIT 编译器跳入执行即时编译过 # 程。 # 0020e6ec 0051f3e2 mscorwks!PreStubWorker(class PrestubMethodFrame * pPFrame = 0x0020e71c)+0x2de [c:\sscli20\clr\src\vm\prestub.cpp @ 662] 0020e748 79366025 0x51f3e2 0020e750 793660a4 mscorwks!CallDescrWorkerInternal+0x33 0020e76c 003c2e70 mscorwks!GetThreadGeneric+0x18 00000000 00000000 0x3c2e70 |
接着我们看看类型A的静态构造函数什么时候应该是什么时候被调用,继续 demo.exe的执行。
0:000> g # # 因为static A()和static B()的C#代码差不多,所以编译出来的汇编代码也是差不多 # 0:000> !u 04e4fed8 Normal JIT generated code A..cctor() Begin 04e4fed8, size 65 >>> 04e4fed8 cc int 3 04e4fed9 55 push ebp 04e4feda 8bec mov ebp,esp 04e4fedc 56 push esi 04e4fedd 33f6 xor esi,esi 04e4fedf 56 push esi 04e4fee0 51 push ecx 04e4fee1 52 push edx 04e4fee2 b901000000 mov ecx,1 04e4fee7 6a00 push 0 04e4fee9 e2fc loop 04e4fee7 04e4feeb b8d42b3c00 mov eax,3C2BD4h 04e4fef0 8b00 mov eax,dword ptr [eax] 04e4fef2 85c0 test eax,eax 04e4fef4 0f8407000000 je 04e4ff01 04e4fefa b860ad4579 mov eax,offset mscorwks!JIT_DbgIsJustMyCode (7945ad60) 04e4feff ffd0 call eax 04e4ff01 90 nop 04e4ff02 fc cld 04e4ff03 90 nop 04e4ff04 90 nop # # 32D1200是“aaaaaaa”的地址 # 04e4ff05 b800122d03 mov eax,32D1200h 04e4ff0a 8b00 mov eax,dword ptr [eax] # # 3C2F54是类型A 的MethodTable地址 # 04e4ff0c b9542f3c00 mov ecx,3C2F54h 04e4ff11 50 push eax 04e4ff12 b880e04479 mov eax,offset mscorwks!JIT_InitClass (7944e080) 04e4ff17 ffd0 call eax # # 32D0D3C是类型A 的静态变量保存的地址 # 04e4ff19 b83c0d2d03 mov eax,32D0D3Ch 04e4ff1e 50 push eax 04e4ff1f b850bfa179 mov eax,offset mscorejt!STSFLD_REF_helper (79a1bf50) 04e4ff24 ffd0 call eax 04e4ff26 90 nop 04e4ff27 54 push esp 04e4ff28 55 push ebp 04e4ff29 b814000000 mov eax,14h 04e4ff2e 50 push eax 04e4ff2f b8e091a179 mov eax,offset mscorejt!check_stack (79a191e0) 04e4ff34 ffd0 call eax 04e4ff36 8b75fc mov esi,dword ptr [ebp-4] 04e4ff39 8be5 mov esp,ebp 04e4ff3b 5d pop ebp 04e4ff3c c3 ret |
接着我们看看A的静态构造函数又是什么时候被调用的,未完待续……,实在是太长了,后面的分析写续集吧!对不住各位兄弟姐妹了,:(
本文转自 donjuan 博客园博客,原文链接:http://www.cnblogs.com/killmyday/archive/2009/10/19/1585938.html ,如需转载请自行联系原作者