关于C#静态函数什么时候被调用的问题

简介:

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的确是先即时编译( JITB的静态构造函数,然后再即时编译A的静态构造函数。

 

第二触发A的静态构造函数的编译是由执行B的静态构造函数的过程所引起的。

 

第三的静态构造函数即时编译完成以后,立即就被调用了,虽然这个时候B的静态构造函数只被执行了一半。

 

下面是static B()被最后JIT成的汇编代码,为了各位网友阅读方便,我在关键的地方加了一些注释描述代码的意思。

# 04e4fe60static B最后被 JIT的代码在内存存放的地址通过查看MethodDesc获取到。

至于如何获取MethodDesc以及MethodDesc是什么……属于背景知识。

0:000> !u 04e4fe60

Normal JIT generated code

B..cctor()

Begin 04e4fe60, size 65

由于我设置参数COMPlus_JitHalt的值为.cctor,即通知CLRJIT完成任何一个类型的静

态构造函数后,立即中断托管程序(当然还有CLR本身)的执行

#

所以CLRJIT完成以后,特意加上了一个断点。

>>> 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]

# 3C2F54AMethodTable地址什么是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的静态成员所分配的内存空间,当然啦,我现在说你

肯定不相信,一会我会演示查看这段内存的方法。

#

注意,32D0D3CA的静态成员地址,不是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

 

上面关于的静态构造函数生成的汇编代码已经解释完毕了,现在的问题是,我们是怎样到达这里的?那当然是看堆栈啦,由于堆栈的调用次序和显示的顺序刚好相反,所以下面的注释序号是反向的:

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类型MethodTable地址

#

04e4ff0c b9542f3c00      mov     ecx,3C2F54h

04e4ff11 50              push    eax

04e4ff12 b880e04479      mov     eax,offset mscorwks!JIT_InitClass (7944e080)

04e4ff17 ffd0            call    eax

#

# 32D0D3C类型的静态变量保存的地址

#

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   ,如需转载请自行联系原作者

相关文章
|
10月前
我应该使用按值调用还是按引用调用?
我应该使用按值调用还是按引用调用?
|
10月前
|
Java
java方法重载
java方法重载
53 0
|
10月前
|
C++
【为什么】构造函数中可以调用虚函数吗?
【为什么】构造函数中可以调用虚函数吗?
|
C++
同样一句代码,在类内调用,跟类外调用结果不同?
同样一句代码,在类内调用,跟类外调用结果不同?
62 0
|
编译器 C++
【C++要笑着学】类的默认成员函数详解 | 构造函数 | 析构函数 | 构造拷贝函数(一)
朋友们好啊,今天终于更新了。我是柠檬叶子C,本章将继续讲解C++中的面向对象的知识点,本篇主要讲解默认成员函数中的构造函数、析构函数和拷贝构造函数。还是和以前一样,我们将由浅入深地去讲解,以 "初学者" 的角度去探索式地学习。会一步步地推进讲解,而不是直接把枯燥的知识点倒出来,应该会有不错的阅读体验。如果觉得不错,可以 "一键三连" 支持一下博主!你们的关注就是我更新的最大动力!Thanks ♪ (・ω・)ノ
103 0
【C++要笑着学】类的默认成员函数详解 | 构造函数 | 析构函数 | 构造拷贝函数(一)
|
安全 编译器 C++
【C++要笑着学】类的默认成员函数详解 | 构造函数 | 析构函数 | 构造拷贝函数(二)
朋友们好啊,今天终于更新了。我是柠檬叶子C,本章将继续讲解C++中的面向对象的知识点,本篇主要讲解默认成员函数中的构造函数、析构函数和拷贝构造函数。还是和以前一样,我们将由浅入深地去讲解,以 "初学者" 的角度去探索式地学习。会一步步地推进讲解,而不是直接把枯燥的知识点倒出来,应该会有不错的阅读体验。如果觉得不错,可以 "一键三连" 支持一下博主!你们的关注就是我更新的最大动力!Thanks ♪ (・ω・)ノ
73 0
【C++要笑着学】类的默认成员函数详解 | 构造函数 | 析构函数 | 构造拷贝函数(二)
|
C语言 C++
C++ 构造函数+析构函数+函数参数的传递
C++ 构造函数+析构函数+函数参数的传递
103 0
C++ 构造函数+析构函数+函数参数的传递
|
小程序 编译器 C语言
C调用C++类成员函数--实例
C与C++调用问题原因主要在于C编译器和C++编译器的不同。C是过程式语言,C编译器编译后,函数在符号库中就是函数名,没有其他任何附加信息。而C++是对象式语言,支持函数重载,C++编译器编译后,在符号库中的名字包含了函数名、函数参数类型和函数返回类型等。因此,当两者混合编译时,会相互找不到对象。
477 0
C调用C++类成员函数--实例
|
编译器 C++
C++不要在构造或析构函数中调用虚函数
C++不要在构造或析构函数中调用虚函数
241 0
|
Java 编译器
Java方法02——方法的调用与重载
Java方法02——方法的调用与重载
90 0
Java方法02——方法的调用与重载