当前位置: 欣欣网 > 码农

.NET9 AOT编译器ILC--约定

2024-02-29码农

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




前言

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

约定

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 * *):00007FF72422578048 89 54 24 10 mov qword ptr [rsp+10h],rdx 00007FF72422578589 4C 24 08 mov dword ptr [rsp+8],ecx 00007FF72422578957 push rdi 00007FF72422578A48 83 EC 30 sub rsp,30h 00007FF72422578E48 8D 0D 70 48 73 00 lea rcx,[__98D9DF53_main@cpp (07FF72495A005h)] 00007FF724225795E8 16 F2 09 00 call __CheckForDebuggerJustMyCode (07FF7242C49B0h) 00007FF72422579AE8 F1 FE FF FF call InitializeRuntime (07FF724225690h) 00007FF72422579F89 44 24 20 mov dword ptr [initval],eax 00007FF7242257A383 7C 24 20 00 cmp dword ptr [initval],0 00007FF7242257A874 06 je wmain+30h (07FF7242257B0h) 00007FF7242257AA8B 44 24 20 mov eax,dword ptr [initval] 00007FF7242257AEEB 0E jmp wmain+3Eh (07FF7242257BEh) 00007FF7242257B048 8B 54 24 48 mov rdx,qword ptr [argv] 00007FF7242257B58B 4C 24 40 mov ecx,dword ptr [argc] 00007FF7242257B9E8 32 4A 39 00 call repro__Module___StartupCodeMain (07FF7245BA1F0h) 00007FF7242257BE48 83 C4 30 add rsp,30h 00007FF7242257C25F pop rdi 00007FF7242257C3C3 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

publicvoid AddCompilationRoots(IRootingServiceProvider rootProvider) { MethodDesc mainMethod = _module.EntryPoint;if (mainMethod == null)thrownew 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解析出来

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

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

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

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

// [repro]<Module>.StartupCodeMain(int32,native int)0: 55 push rbp1: 48 83 ec 40 sub rsp, 645: 48 8d 6c 24 40 lea rbp, [rsp + 64]a: 33 c0 xor eax, eaxc: 48 89 45 e0 mov qword ptr [rbp - 32], rax10: 48 89 45 e8 mov qword ptr [rbp - 24], rax14: 89 4d 10 mov dword ptr [rbp + 16], ecx17: 48 89 55 18 mov qword ptr [rbp + 24], rdx1b: 48 8d 4d e0 lea rcx, [rbp - 32]1f: e8 00 00 00 00 call 0 // RhpReversePInvoke24: 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], rax54: 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, 162: 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], eax79: 8b 4d f4 mov ecx, dword ptr [rbp - 12]7c: 89 4d f0 mov dword ptr [rbp - 16], ecx7f: 48 8d 4d e0 lea rcx, [rbp - 32]83: e8 00 00 00 00 call 0 // RhpReversePInvokeReturn88: 8b 45 f0 mov eax, dword ptr [rbp - 16]8b: 48 83 c4 40 add rsp, 648f: 5d pop rbp90: c3 ret

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

6c: e8 00 00 00 00 call0 // [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):00007FF7245BA1F055 push rbp 00007FF7245BA1F148 83 EC 40 sub rsp,40h //省略部分00007FF7245BA20FE8 AC 4A C7 FF call RhpReversePInvoke (07FF72422ECC0h) 00007FF7245BA214E8 D7 FB F1 FF call S_P_CoreLib_Internal_Runtime_CompilerHelpers_LibraryInitializer__InitializeLibrary (07FF7244D9DF0h) 00007FF7245BA219E8 F2 2F DE FF call S_P_StackTraceMetadata_Internal_Runtime_CompilerHelpers_LibraryInitializer__InitializeLibrary (07FF72439D210h) 00007FF7245BA21EE8 5D 79 F8 FF call S_P_TypeLoader_Internal_Runtime_CompilerHelpers_LibraryInitializer__InitializeLibrary (07FF724541B80h) 00007FF7245BA223E8 98 82 F4 FF call S_P_Reflection_Execution_Internal_Runtime_CompilerHelpers_LibraryInitializer__InitializeLibrary (07FF7245024C0h) 00007FF7245BA2288B 4D 10 mov ecx,dword ptr [rbp+10h] 00007FF7245BA22B48 8B 55 18 mov rdx,qword ptr [rbp+18h] 00007FF7245BA22FE8 6C 0E F2 FF call S_P_CoreLib_Internal_Runtime_CompilerHelpers_StartupCodeHelpers__InitializeCommandLineArgsW (07FF7244DB0A0h) 00007FF7245BA23448 8D 0D 55 67 14 00 lea rcx,[repro__Module_::`vftable' (07FF724700990h)] 00007FF7245BA23BE8 C0 17 F2 FF call S_P_CoreLib_Internal_Runtime_CompilerHelpers_LdTokenHelpers__GetRuntimeTypeHandle (07FF7244DBA00h) 00007FF7245BA24048 89 45 F8 mov qword ptr [rbp-8],rax 00007FF7245BA24448 8B 4D F8 mov rcx,qword ptr [rbp-8] 00007FF7245BA248E8 23 FC F1 FF call S_P_CoreLib_Internal_Runtime_CompilerHelpers_StartupCodeHelpers__InitializeEntryAssembly (07FF7244D9E70h) 00007FF7245BA24DB9 01 00 00 00 mov ecx,1 00007FF7245BA252E8 69 0F F2 FF call S_P_CoreLib_Internal_Runtime_CompilerHelpers_StartupCodeHelpers__InitializeApartmentState (07FF7244DB1C0h) 00007FF7245BA257E8 44 04 F2 FF call S_P_CoreLib_Internal_Runtime_CompilerHelpers_StartupCodeHelpers__RunModuleInitializers (07FF7244DA6A0h) 00007FF7245BA25CE8 7F FF FF FF call repro__Module___MainMethodWrapper (07FF7245BA1E0h) //省略部分00007FF7245BA27B48 83 C4 40 add rsp,40h 00007FF7245BA27F5D pop rbp 00007FF7245BA280C3 ret

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

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

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

repro__Module___MainMethodWrapper 函数

00007FF7245BA1E055 push rbp 00007FF7245BA1E148 8B EC mov rbp,rsp 00007FF7245BA1E490 nop 00007FF7245BA1E55D pop rbp 00007FF7245BA1E6 E9 A5 7F F4 FF jmp repro_Program__Main (07FF724502190h)

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

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

往期精彩回顾

起学习