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

相关文章
|
4月前
|
开发框架 .NET C#
【Azure Developer】C# / .NET 静态函数中this关键字的作用
【Azure Developer】C# / .NET 静态函数中this关键字的作用
|
5月前
|
开发框架 .NET C#
【Azure Developer】C# / .NET 静态函数中this关键字的作用
在C#中,`this`关键字用于扩展方法,允许向已有类型添加功能而不修改其源代码。扩展方法必须在静态类中定义,且第一个参数使用`this`修饰,如`public static XElement AcquireElement(this XContainer container, string name, bool addFirst = false)`。这种方式增强了代码的可读性和类型的安全性,尤其在处理第三方库时。
|
C# 索引
C#中修饰符用法“virtual(虚拟函数)、override(重载函数)、static(静态函数)、abstract(抽象函数)、sealed(密封函数)”
1、virtual用于修饰方法、属性、索引器或者事件声明,并使它们可以在派生类中被重写。virtual不能与static、abstract、private或者override修饰符一起使用。 声明为virtual的方法被重写时,派生类中可以使用base关键字访问父类中的publid,protected成员。
1616 0
|
7月前
|
开发框架 前端开发 .NET
C#编程与Web开发
【4月更文挑战第21天】本文探讨了C#在Web开发中的应用,包括使用ASP.NET框架、MVC模式、Web API和Entity Framework。C#作为.NET框架的主要语言,结合这些工具,能创建动态、高效的Web应用。实际案例涉及企业级应用、电子商务和社交媒体平台。尽管面临竞争和挑战,但C#在Web开发领域的前景将持续拓展。
209 3
|
7月前
|
SQL 开发框架 安全
C#编程与多线程处理
【4月更文挑战第21天】探索C#多线程处理,提升程序性能与响应性。了解C#中的Thread、Task类及Async/Await关键字,掌握线程同步与安全,实践并发计算、网络服务及UI优化。跟随未来发展趋势,利用C#打造高效应用。
204 3
|
1月前
|
C# 开发者
C# 一分钟浅谈:Code Contracts 与契约编程
【10月更文挑战第26天】本文介绍了 C# 中的 Code Contracts,这是一个强大的工具,用于通过契约编程增强代码的健壮性和可维护性。文章从基本概念入手,详细讲解了前置条件、后置条件和对象不变量的使用方法,并通过具体代码示例进行了说明。同时,文章还探讨了常见的问题和易错点,如忘记启用静态检查、过度依赖契约和性能影响,并提供了相应的解决建议。希望读者能通过本文更好地理解和应用 Code Contracts。
34 3
|
28天前
|
设计模式 C# 图形学
Unity 游戏引擎 C# 编程:一分钟浅谈
本文介绍了在 Unity 游戏开发中使用 C# 的基础知识和常见问题。从 `MonoBehavior` 类的基础用法,到变量和属性的管理,再到空引用异常、资源管理和性能优化等常见问题的解决方法。文章还探讨了单例模式、事件系统和数据持久化等高级话题,旨在帮助开发者避免常见错误,提升游戏开发效率。
43 4
|
3月前
|
API C#
C# 一分钟浅谈:文件系统编程
在软件开发中,文件系统操作至关重要。本文将带你快速掌握C#中文件系统编程的基础知识,涵盖基本概念、常见问题及解决方法。文章详细介绍了`System.IO`命名空间下的关键类库,并通过示例代码展示了路径处理、异常处理、并发访问等技巧,还提供了异步API和流压缩等高级技巧,帮助你写出更健壮的代码。
48 2
|
2月前
|
安全 C# 数据安全/隐私保护
实现C#编程文件夹加锁保护
【10月更文挑战第16天】本文介绍了两种用 C# 实现文件夹保护的方法:一是通过设置文件系统权限,阻止普通用户访问;二是使用加密技术,对文件夹中的文件进行加密,防止未授权访问。提供了示例代码和使用方法,适用于不同安全需求的场景。
133 0
|
3月前
|
安全 程序员 编译器
C#一分钟浅谈:泛型编程基础
在现代软件开发中,泛型编程是一项关键技能,它使开发者能够编写类型安全且可重用的代码。C# 自 2.0 版本起支持泛型编程,本文将从基础概念入手,逐步深入探讨 C# 中的泛型,并通过具体实例帮助理解常见问题及其解决方法。泛型通过类型参数替代具体类型,提高了代码复用性和类型安全性,减少了运行时性能开销。文章详细介绍了如何定义泛型类和方法,并讨论了常见的易错点及解决方案,帮助读者更好地掌握这一技术。
82 11