有小伙伴被面试官问到这个问题,本篇彻底解析下这个问题。为了彻底点,注意本篇是最底层的.Net 7 RC CLR
运行模型(汇编)为基础进行全局剖析,局部业务分析。
目的非手段
这两个函数比较特殊的存在,.Ctor
是非静态默认实例化。.CCtor
是静态默认实例化。这两个函数伴随着.Net
任何对象的实例化都自动存在于这个对象当中。
跟踪.CCtor
可以在全局静态对象下断点,观察它的里面运行。跟踪.Ctor
可以通过!name2ee
模块 模块.类名..Ctor
找到JITTED Code Address
,观察它的运行。正如本段题所说,这只是手段,不是目的。所以下面看目的。
.Ctor目的
先来看下非静态默认构造函数.Ctor
。上一段代码:
- internal class Program
- {
- public class ABC
- {
- }
- static void Main(string[] args)
- {
- ABC abc = new ABC();
- Console.ReadLine();
- }
- }
直接给它反编译:
- 00007FFDF2FA03B0 55 push rbp
- 00007FFDF2FA03B1 48 83 EC 40 sub rsp,40h
- 00007FFDF2FA03B5 48 8D 6C 24 40 lea rbp,[rsp+40h]
- 00007FFDF2FA03BA C5 D8 57 E4 vxorps xmm4,xmm4,xmm4
- 00007FFDF2FA03BE C5 FA 7F 65 E8 vmovdqu xmmword ptr [rbp-18h],xmm4
- 00007FFDF2FA03C3 33 C0 xor eax,eax
- 00007FFDF2FA03C5 48 89 45 F8 mov qword ptr [rbp-8],rax
- 00007FFDF2FA03C9 48 89 4D 10 mov qword ptr [rbp+10h],rcx
- 00007FFDF2FA03CD 83 3D BC E9 19 00 00 cmp dword ptr [7FFDF313ED90h],0
- 00007FFDF2FA03D4 74 05 je 00007FFDF2FA03DB
- 00007FFDF2FA03D6 E8 B5 BF 79 5E call JIT_DbgIsJustMyCode (07FFE5173C390h)
- 00007FFDF2FA03DB 90 nop
- 00007FFDF2FA03DC 48 B9 30 F6 5B F3 FD 7F 00 00 mov rcx,7FFDF35BF630h
- 00007FFDF2FA03E6 E8 75 7C C1 5E call JIT_TrialAllocSFastMP_InlineGetThread (07FFE51BB8060h)
- 00007FFDF2FA03EB 48 89 45 F0 mov qword ptr [rbp-10h],rax
- 00007FFDF2FA03EF 48 8B 4D F0 mov rcx,qword ptr [rbp-10h]
- // 这个地方是调用了.Ctor
- 00007FFDF2FA03F3 FF 15 0F 8D 60 00 call qword ptr [7FFDF35A9108h]
- 00007FFDF2FA03F9 48 8B 45 F0 mov rax,qword ptr [rbp-10h]
- 00007FFDF2FA03FD 48 89 45 F8 mov qword ptr [rbp-8],rax
- 00007FFDF2FA0401 FF 15 A9 93 60 00 call qword ptr [7FFDF35A97B0h]
- 00007FFDF2FA0407 48 89 45 E8 mov qword ptr [rbp-18h],rax
- 00007FFDF2FA040B 90 nop
- 00007FFDF2FA040C 90 nop
- 00007FFDF2FA040D 48 83 C4 40 add rsp,40h
- 00007FFDF2FA0411 5D pop rbp
- 00007FFDF2FA0412 C3 ret
调用.Ctor
的地方注释了下,如果直接进入会调用到PrecodeFixupThunk
。所以这里需要在PreStubWorker
下断点。一路跟踪下去发现这个.Ctor
是利用预备的IL代码,让RyuJIt
对它进行一个编译
.Ctor
调用堆栈:
- coreclr.dll!MethodDesc::JitCompileCodeLocked 行 952 C++
- coreclr.dll!MethodDesc::JitCompileCodeLockedEventWrapper 行 823 C++
- coreclr.dll!MethodDesc::JitCompileCode 行 763 C++
- coreclr.dll!MethodDesc::PrepareILBasedCode 行 426 C++
- coreclr.dll!MethodDesc::PrepareCode 行 323 C++
- coreclr.dll!CodeVersionManager::PublishVersionableCodeIfNecessary 行 1698 C++
- coreclr.dll!MethodDesc::DoPrestub 行 2109 C++
- coreclr.dll!PreStubWorker 行 1938
- coreclr.dll!ThePreStub(
到JitCompileCodeLocked
里面调用了UnsafeJitFunction
为止,因为后面都是RyuJit的
复杂编译过程,此处不述。
我们来看下UnsafeJitFunction
返回的pCode
地址处的汇编代码:
- 00007FFDF2F80430 55 push rbp
- 00007FFDF2F80431 57 push rdi
- 00007FFDF2F80432 48 83 EC 28 sub rsp,28h
- 00007FFDF2F80436 48 8D 6C 24 30 lea rbp,[rsp+30h]
- 00007FFDF2F8043B 48 89 4D 10 mov qword ptr [rbp+10h],rcx
- 00007FFDF2F8043F 83 3D 4A E9 19 00 00 cmp dword ptr [7FFDF311ED90h],0
- 00007FFDF2F80446 74 05 je 00007FFDF2F8044D
- 00007FFDF2F80448 E8 43 BF 7B 5E call JIT_DbgIsJustMyCode (07FFE5173C390h)
- 00007FFDF2F8044D 48 8B 4D 10 mov rcx,qword ptr [rbp+10h]
- 00007FFDF2F80451 FF 15 D9 0B E5 FF call qword ptr [7FFDF2DD1030h]
- 00007FFDF2F80457 90 nop
- 00007FFDF2F80458 90 nop
- 00007FFDF2F80459 48 83 C4 28 add rsp,28h
- 00007FFDF2F8045D 5F pop rdi
- 00007FFDF2F8045E 5D pop rbp
- 00007FFDF2F8045F C3 ret
它里面就调用了一个Call
,也就是这句话:
- call qword ptr [7FFDF2DD1030h]
这个十六进制的7FFDF2DD1030h
是个啥呢?继续跟进下:0x00007FFDF2DD1030 00007ffe50357230
它里面包含了一个地址00007ffe50357230
看下这个地址的汇编代码:
- 00007FFE50357230 C3 ret
它直接返回了。
所以这得出了一个什么结论呢?也就是说在当前这个例子中,.Ctor啥都没做。
.CCtor目的
来看下静态的默认构造函数干了些啥。先上代码:
- internal class Program
- {
- static string a ="abcd";
- static void Main(string[] args)
- {
- string i = a;
- Console.WriteLine(a);
- Console.ReadLine();
- }
- }
同样反编译下:
- 00007FFDF01903B0 55 push rbp
- 00007FFDF01903B1 57 push rdi
- 00007FFDF01903B2 48 83 EC 28 sub rsp,28h
- 00007FFDF01903B6 48 8D 6C 24 30 lea rbp,[rsp+30h]
- 00007FFDF01903BB 33 C0 xor eax,eax
- 00007FFDF01903BD 48 89 45 F0 mov qword ptr [rbp-10h],rax
- 00007FFDF01903C1 48 89 4D 10 mov qword ptr [rbp+10h],rcx
- 00007FFDF01903C5 83 3D C4 E9 19 00 00 cmp dword ptr [7FFDF032ED90h],0
- 00007FFDF01903CC 74 05 je 00007FFDF01903D3
- 00007FFDF01903CE E8 BD BF 7D 5E call JIT_DbgIsJustMyCode (07FFE4E96C390h)
- 00007FFDF01903D3 90 nop
- 00007FFDF01903D4 48 B9 60 EF 32 F0 FD 7F 00 00 mov rcx,7FFDF032EF60h
- 00007FFDF01903DE BA 04 00 00 00 mov edx,4
- // 可以看到这个 string 静态对象并没有调用.CCtor。
- // 那是否说明上面的说法不对呢?注意看,他实际调用了
- // JIT_GetSharedNonGCStaticBase_SingleAppDomain,
- // 而这个就是关键所在
- 00007FFDF01903E3 E8 48 7E C5 5E call JIT_GetSharedNonGCStaticBase_SingleAppDomain (07FFE4EDE8230h)
- 00007FFDF01903E8 8B 0D AA EB 19 00 mov ecx,dword ptr [7FFDF032EF98h]
- 00007FFDF01903EE FF 15 7C 94 60 00 call qword ptr [7FFDF0799870h]
- 00007FFDF01903F4 90 nop
- 00007FFDF01903F5 FF 15 9D 93 60 00 call qword ptr [7FFDF0799798h]
- 00007FFDF01903FB 48 89 45 F0 mov qword ptr [rbp-10h],rax
- 00007FFDF01903FF 90 nop
- 00007FFDF0190400 90 nop
- 00007FFDF0190401 48 83 C4 28 add rsp,28h
- 00007FFDF0190405 5F pop rdi
- 00007FFDF0190406 5D pop rbp
- 00007FFDF0190407 C3 ret
- 00007FFDF0190408 19 06 sbb dword ptr [rsi],eax
看这段代码上面的注释,这段代码里面并没有.CCtor
被调用的痕迹。而它的奥秘在JIT_GetSharedNonGCStaticBase_SingleAppDomain
函数里面。
JIT_GetSharedNonGCStaticBase_SingleAppDomain
又调用了JIT_GetSharedNonGCStaticBase_Helper
看下堆栈
- > coreclr.dll!MethodTable::RunClassInitEx 行 3591 C++
- coreclr.dll!MethodTable::DoRunClassInitThrowing 行 3792 C++
- coreclr.dll!MethodTable::CheckRunClassInitThrowing 行 3929 C++
- coreclr.dll!JIT_GetSharedNonGCStaticBase_Helper 行 1401 C++
函数RunClassInitEx
代码如下:
- BOOL MethodTable::RunClassInitEx(OBJECTREF *pThrowable)
- {
- //为了方便观看 此处省略部分代码
- PCODE pCctorCode = pCanonMT->GetSlot(pCanonMT->GetClassConstructorSlot());
- //为了方便观看 此处省略部分代码
- PREPARE_NONVIRTUAL_CALLSITE_USING_CODE(pCctorCode);
- DECLARE_ARGHOLDER_ARRAY(args, 0);
- CATCH_HANDLER_FOUND_NOTIFICATION_CALLSITE;
- CALL_MANAGED_METHOD_NORET(args);
- //为了方便观看 此处省略部分代码
变量pCctorCode
就是.CCtor
的函数头地址。而后面的一堆的宏定义实际上是调用了函数DispatchCallSimple
,而DispatchCallSimple
又调用了CallDescrWorkerWithHandler
然后又调用了PrecodeFixupThunk
下面调用了PreStubWorker
PreStubWorker
通过call rax
命令跳转到调用的函数的函数头地址,比如本例的.CCtor
函数头的地址。
- 00007FFE8BB289C0 E8 DB FE 8F FF call PreStubWorker (07FFE8B4288A0h)
- 00007FFE8BB289C5 66 0F 6F 44 24 20 movdqa xmm0,xmmword ptr [rsp+20h]
- 00007FFE8BB289CB 66 0F 6F 4C 24 30 movdqa xmm1,xmmword ptr [rsp+30h]
- 00007FFE8BB289D1 66 0F 6F 54 24 40 movdqa xmm2,xmmword ptr [rsp+40h]
- 00007FFE8BB289D7 66 0F 6F 5C 24 50 movdqa xmm3,xmmword ptr [rsp+50h]
- 00007FFE8BB289DD 48 8B 8C 24 B0 00 00 00 mov rcx,qword ptr [rsp+0B0h]
- 00007FFE8BB289E5 48 8B 94 24 B8 00 00 00 mov rdx,qword ptr [rsp+0B8h]
- 00007FFE8BB289ED 4C 8B 84 24 C0 00 00 00 mov r8,qword ptr [rsp+0C0h]
- 00007FFE8BB289F5 4C 8B 8C 24 C8 00 00 00 mov r9,qword ptr [rsp+0C8h]
- 00007FFE8BB289FD 48 83 C4 68 add rsp,68h
- 00007FFE8BB28A01 5F pop rdi
- 00007FFE8BB28A02 5E pop rsi
- 00007FFE8BB28A03 5B pop rbx
- 00007FFE8BB28A04 5D pop rbp
- 00007FFE8BB28A05 41 5C pop r12
- 00007FFE8BB28A07 41 5D pop r13
- 00007FFE8BB28A09 41 5E pop r14
- 00007FFE8BB28A0B 41 5F pop r15
- // 这个rax 就是 .CCtor的函数头的地址
- 00007FFE8BB28A0D 48 FF E0 jmp rax
jmp rax
跳转到了如下:
00007FFE2CFE8888 FF 25 FA 0F 00 00 jmp qword ptr [7FFE2CFE9888h]
7FFE2CFE9888h
地址的值是00007FFE8A50C7A0
注意这句代码
- static string a ="abcd";
它实际上被编译成了一个函数,当运行到.CCtor
的时候,会调用它,然后对它进行赋值abcd
- >>> 00007ffe`06ac29e0 55 push rbp
- 00007ffe`06ac29e1 4883ec20 sub rsp,20h
- 00007ffe`06ac29e5 488d6c2420 lea rbp,[rsp+20h]
- 00007ffe`06ac29ea 833d9f410c0000 cmp dword ptr [00007ffe`06b86b90],0
- 00007ffe`06ac29f1 7405 je ConsoleApp3!ConsoleApp3.Program..cctor+0x18 (00007ffe`06ac29f8)
- 00007ffe`06ac29f3 e8e8a4cd5f call coreclr!JIT_DbgIsJustMyCode (00007ffe`6679cee0)
- 00007ffe`06ac29f8 48bad83000186c020000 mov rdx,26C180030D8h
- 00007ffe`06ac2a02 488b12 mov rdx,qword ptr [rdx]
- 00007ffe`06ac2a05 48b9902e00186c020000 mov rcx,26C18002E90h
- 00007ffe`06ac2a0f e8fc85bb5f call coreclr!JIT_CheckedWriteBarrier (00007ffe`6667b010)
- 00007ffe`06ac2a14 90 nop
- 00007ffe`06ac2a15 4883c420 add rsp,20h
- 00007ffe`06ac2a19 5d pop rbp
- 00007ffe`06ac2a1a c3 ret
JIT_CheckedWriteBarrier
的原型如下:
- extern "C" HCIMPL2_RAW(VOID, JIT_CheckedWriteBarrier, Object **dst, Object *ref)
很明显,他这就是把ref
指向的object
完整的传递给dst
。也就是赋值给静态字符串a
。寄存器rcx
表示dst
,rdx
表示ref
。此处可以通过!dumpobj rdx
来查被看对象。
那么总结下,.CCtor
的作用就是把静态的全局变量对象进行一个初始化,这个结果也说明,静态全局变量不是在CLR
初始化的时候初始化,而是在当前类的.CCtor
里面初始化的。