.NET9 AOT编译器ILC--约定

点击上方蓝字 江湖评谈设为关注

758740dd84fa9031e9d50a33f8bb1bf7.png

前言

.NET7之后的AOT编译器ILC(ILCompiler)是根据CLR/JIT(C++),用C#代码重写的一个新编译器。注意它不是之前的CoreRT项目。.NET9里面AOT编译器更进一步发展,本篇主要来看下ILC编译器机器码的生成以及引导文件(AOT引导程序)里面一些符号的设置。看下约定大于配置的骚操,简化项目中各种不必要的文件配置。

约定

ILC生成的是目标文件,并不是直接可执行的文件。这个目标文件里面包含了可执行文件需要的所有机器码符号以及机器码内容。当引导程序进行链接,根据目标文件生成可执行文件的时候。会对这些符号以及内容进行解析,在相应的平台上(MacOS/Linux/Win)进行执行。在这个中间,ILC编译器和引导程序进行了一个约定,以便双方进行约定式调用。

比如:要运行.NET程序,则需要一个运行环境。引导程序初始化运行时之后,就会调用约定俗称的函数:模块名__Module___StartupCodeMain。

这是什么意思呢?比如托管DLL的名称是repro.dll。调用托管Main入口的AOT引导程序里面函数的名称则为:repro___Module___StartupCodeMain。这是约定俗成的规矩。

上面一共讲了两件事:初始化.NET运行环境,以及调用约定俗成函数(模块名__Module___StartupCodeMain)。下面简略看下这些代码:

repro.exe!wmain(int, wchar_t * *):
00007FF724225780 48 89 54 24 10       mov         qword ptr [rsp+10h],rdx  
00007FF724225785 89 4C 24 08          mov         dword ptr [rsp+8],ecx  
00007FF724225789 57                   push        rdi  
00007FF72422578A 48 83 EC 30          sub         rsp,30h  
00007FF72422578E 48 8D 0D 70 48 73 00 lea         rcx,[__98D9DF53_main@cpp (07FF72495A005h)]  
00007FF724225795 E8 16 F2 09 00       call        __CheckForDebuggerJustMyCode (07FF7242C49B0h)  
00007FF72422579A E8 F1 FE FF FF       call        InitializeRuntime (07FF724225690h)  
00007FF72422579F 89 44 24 20          mov         dword ptr [initval],eax  
00007FF7242257A3 83 7C 24 20 00       cmp         dword ptr [initval],0  
00007FF7242257A8 74 06                je          wmain+30h (07FF7242257B0h)  
00007FF7242257AA 8B 44 24 20          mov         eax,dword ptr [initval]  
00007FF7242257AE EB 0E                jmp         wmain+3Eh (07FF7242257BEh)  
00007FF7242257B0 48 8B 54 24 48       mov         rdx,qword ptr [argv]  
00007FF7242257B5 8B 4C 24 40          mov         ecx,dword ptr [argc]  
00007FF7242257B9 E8 32 4A 39 00       call        repro__Module___StartupCodeMain (07FF7245BA1F0h)  
00007FF7242257BE 48 83 C4 30          add         rsp,30h  
00007FF7242257C2 5F                   pop         rdi  
00007FF7242257C3 C3                   ret

repro.exe是AOT最终的独立可执行exe文件。repro名称根据托管repro.dll来的。wmain则是AOT程序的非托管入口。初始化运行时:

00007FF72422579A E8 F1 FE FF FF  call InitializeRuntime (07FF724225690h)

调用约定俗成函数:

00007FF7242257B9 E8 32 4A 39 00  call  repro__Module___StartupCodeMain (07FF7245BA1F0h)

ILC

约定的函数:模块名__Module___StartupCodeMain,它需要在ILC里面进行生成符号和编译,才能够在引导程序里面进行执行。所以这里需要看下它在ILC里面的经过。

ILC里面会构建一个{[模块名]<Module>.StartupCodeMain(int32,native int)}的节点,比如托管repro.dll,则会构建{[repro]<Module>.StartupCodeMain(int32,native int)}节点。它这个符号根据约定在引导文件里面变成了repro__Module___StartupCodeMain

public void AddCompilationRoots(IRootingServiceProvider rootProvider)
 {
     MethodDesc mainMethod = _module.EntryPoint;
     if (mainMethod == null)
         throw new Exception("No managed entrypoint defined for executable module");
     TypeDesc owningType = _module.GetGlobalModuleType();
     var startupCodeMain = new StartupCodeMainMethod(owningType, mainMethod, _libraryInitializers, _generateLibraryAndModuleInitializers);
     rootProvider.AddCompilationRoot(startupCodeMain, "Startup Code Main Method", ManagedEntryPointMethodName);
 }

这个节点里面包含了MSIL,把里面包含的MSIL解析出来

public void CompileMethod(MethodCodeNode methodCodeNodeNeedingCode, MethodIL methodIL = null)
{
    _methodCodeNode = methodCodeNodeNeedingCode;
    _isFallbackBodyCompilation = methodIL != null;
    methodIL ??= _compilation.GetMethodIL(MethodBeingCompiled);
    try
    {
        CompileMethodInternal(methodCodeNodeNeedingCode, methodIL);
    }
}

然后进行JIT编译,C#里面直接通过Dllimport非托管DLL导入函数调用即可。

[DllImport("jitinterface")]
 private static extern CorJitResult JitCompileMethod(out IntPtr exception,
     IntPtr jit, IntPtr thisHandle, IntPtr callbacks,
     ref CORINFO_METHOD_INFO info, uint flags, out IntPtr nativeEntry, out uint codeSize);

这里的机器码也需要注意一些事项,比如,它会进行一些call的重定位。也就是说刚开始编译出来的call会指向call 0。但是下次编译则会指向正确的函数头地址。这里还是以[repro]<Module>.StartupCodeMain(int32,native int)函数为例

// [repro]<Module>.StartupCodeMain(int32,native int)
0: 55                    push  rbp
1: 48 83 ec 40           sub  rsp, 64
5: 48 8d 6c 24 40        lea  rbp, [rsp + 64]
a: 33 c0                 xor  eax, eax
c: 48 89 45 e0           mov  qword ptr [rbp - 32], rax
10: 48 89 45 e8           mov  qword ptr [rbp - 24], rax
14: 89 4d 10              mov  dword ptr [rbp + 16], ecx
17: 48 89 55 18           mov  qword ptr [rbp + 24], rdx
1b: 48 8d 4d e0           lea  rcx, [rbp - 32]
1f: e8 00 00 00 00        call  0 // RhpReversePInvoke
24: e8 00 00 00 00        call  0 // [S.P.CoreLib]Internal.Runtime.CompilerHelpers.LibraryInitializer.InitializeLibrary()
29: e8 00 00 00 00        call  0 // [S.P.StackTraceMetadata]Internal.Runtime.CompilerHelpers.LibraryInitializer.InitializeLibrary()
2e: e8 00 00 00 00        call  0 // [S.P.TypeLoader]Internal.Runtime.CompilerHelpers.LibraryInitializer.InitializeLibrary()
33: e8 00 00 00 00        call  0 // [S.P.Reflection.Execution]Internal.Runtime.CompilerHelpers.LibraryInitializer.InitializeLibrary()
38: 8b 4d 10              mov  ecx, dword ptr [rbp + 16]
3b: 48 8b 55 18           mov  rdx, qword ptr [rbp + 24]
3f: e8 00 00 00 00        call  0 // [S.P.CoreLib]Internal.Runtime.CompilerHelpers.StartupCodeHelpers.InitializeCommandLineArgsW(int32,char**)
44: 48 8d 0d 00 00 00 00  lea  rcx, [rip] // [repro]<Module>
4b: e8 00 00 00 00        call  0 // [S.P.CoreLib]Internal.Runtime.CompilerHelpers.LdTokenHelpers.GetRuntimeTypeHandle(native int)
50: 48 89 45 f8           mov  qword ptr [rbp - 8], rax
54: 48 8b 4d f8           mov  rcx, qword ptr [rbp - 8]
58: e8 00 00 00 00        call  0 // [S.P.CoreLib]Internal.Runtime.CompilerHelpers.StartupCodeHelpers.InitializeEntryAssembly(RuntimeTypeHandle)
5d: b9 01 00 00 00        mov  ecx, 1
62: e8 00 00 00 00        call  0 // [S.P.CoreLib]Internal.Runtime.CompilerHelpers.StartupCodeHelpers.InitializeApartmentState(ApartmentState)
67: e8 00 00 00 00        call  0 // [S.P.CoreLib]Internal.Runtime.CompilerHelpers.StartupCodeHelpers.RunModuleInitializers()
6c: e8 00 00 00 00        call  0 // [repro]<Module>.MainMethodWrapper()
71: e8 00 00 00 00        call  0 // [S.P.CoreLib]Internal.Runtime.CompilerHelpers.StartupCodeHelpers.Shutdown()
76: 89 45 f4              mov  dword ptr [rbp - 12], eax
79: 8b 4d f4              mov  ecx, dword ptr [rbp - 12]
7c: 89 4d f0              mov  dword ptr [rbp - 16], ecx
7f: 48 8d 4d e0           lea  rcx, [rbp - 32]
83: e8 00 00 00 00        call  0 // RhpReversePInvokeReturn
88: 8b 45 f0              mov  eax, dword ptr [rbp - 16]
8b: 48 83 c4 40           add  rsp, 64
8f: 5d                    pop  rbp
90: c3                    ret

这里面有很多call 0。我们注意到的是[repro]<Module>.MainMethodWrapper()这里也是call 0

6c: e8 00 00 00 00  call  0 // [repro]<Module>.MainMethodWrapper()

下次编译(注意它是并行编译:Parallel.ForEach)的时候会替代真正的函数头到这个call 0地方。

托管Main

在约定俗成函数:模块名__Module___StartupCodeMain里面,又做了两件事情。其一:初始化了System.Private.CoreLib.dll。其二:调用真正的托管Main入口,也即是C# Main入口真正的地方。同样的约定在托管Main的调用处也清晰可见。比如托管Main函数头地址:模块名_Program_Main。模块是repro.dll,那么托管Main函数则为:repro_Program_Main。

reproNative.exe!repro__Module___StartupCodeMain(void):
00007FF7245BA1F0 55                   push        rbp  
00007FF7245BA1F1 48 83 EC 40          sub         rsp,40h  
//省略部分
00007FF7245BA20F E8 AC 4A C7 FF       call        RhpReversePInvoke (07FF72422ECC0h)  
00007FF7245BA214 E8 D7 FB F1 FF       call        S_P_CoreLib_Internal_Runtime_CompilerHelpers_LibraryInitializer__InitializeLibrary (07FF7244D9DF0h)  
00007FF7245BA219 E8 F2 2F DE FF       call        S_P_StackTraceMetadata_Internal_Runtime_CompilerHelpers_LibraryInitializer__InitializeLibrary (07FF72439D210h)  
00007FF7245BA21E E8 5D 79 F8 FF       call        S_P_TypeLoader_Internal_Runtime_CompilerHelpers_LibraryInitializer__InitializeLibrary (07FF724541B80h)  
00007FF7245BA223 E8 98 82 F4 FF       call        S_P_Reflection_Execution_Internal_Runtime_CompilerHelpers_LibraryInitializer__InitializeLibrary (07FF7245024C0h)  
00007FF7245BA228 8B 4D 10             mov         ecx,dword ptr [rbp+10h]  
00007FF7245BA22B 48 8B 55 18          mov         rdx,qword ptr [rbp+18h]  
00007FF7245BA22F E8 6C 0E F2 FF       call        S_P_CoreLib_Internal_Runtime_CompilerHelpers_StartupCodeHelpers__InitializeCommandLineArgsW (07FF7244DB0A0h)  
00007FF7245BA234 48 8D 0D 55 67 14 00 lea         rcx,[repro__Module_::`vftable' (07FF724700990h)]  
00007FF7245BA23B E8 C0 17 F2 FF       call        S_P_CoreLib_Internal_Runtime_CompilerHelpers_LdTokenHelpers__GetRuntimeTypeHandle (07FF7244DBA00h)  
00007FF7245BA240 48 89 45 F8          mov         qword ptr [rbp-8],rax  
00007FF7245BA244 48 8B 4D F8          mov         rcx,qword ptr [rbp-8]  
00007FF7245BA248 E8 23 FC F1 FF       call        S_P_CoreLib_Internal_Runtime_CompilerHelpers_StartupCodeHelpers__InitializeEntryAssembly (07FF7244D9E70h)  
00007FF7245BA24D B9 01 00 00 00       mov         ecx,1  
00007FF7245BA252 E8 69 0F F2 FF       call        S_P_CoreLib_Internal_Runtime_CompilerHelpers_StartupCodeHelpers__InitializeApartmentState (07FF7244DB1C0h)  
00007FF7245BA257 E8 44 04 F2 FF       call        S_P_CoreLib_Internal_Runtime_CompilerHelpers_StartupCodeHelpers__RunModuleInitializers (07FF7244DA6A0h)  
00007FF7245BA25C E8 7F FF FF FF       call        repro__Module___MainMethodWrapper (07FF7245BA1E0h)  
//省略部分
00007FF7245BA27B 48 83 C4 40          add         rsp,40h  
00007FF7245BA27F 5D                   pop         rbp  
00007FF7245BA280 C3                   ret

我们看到这里面有很多S_P开头的函数,它实际上即是System.Private.CoreLib.dll模块的缩写。

调用托管Main入口前一个函数:

00007FF7245BA25C E8 7F FF FF FF call repro__Module___MainMethodWrapper (07FF7245BA1E0h)

repro__Module___MainMethodWrapper 函数

00007FF7245BA1E0 55                   push        rbp  
00007FF7245BA1E1 48 8B EC             mov         rbp,rsp  
00007FF7245BA1E4 90                   nop  
00007FF7245BA1E5 5D                   pop         rbp  
00007FF7245BA1E6 E9 A5 7F F4 FF       jmp         repro_Program__Main (07FF724502190h)

repro托管DLL名称,Program和Main非常熟悉了,这也是约定调用。可以看到它这里面就直接跳转到托管Main函数头地址。

最后推荐下个人的学习交流圈,教学最新的.NET8/9核心CLR/JIT,抛弃陈旧的技术。超级硬核,全网无人能及。欢迎加入一起学习,一起进步。

往期精彩回顾

.NET9核心CLR/JIT学习圈,欢迎加入一起学习

53b29a1d8daee90226aee20548e83b5f.jpeg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值