當前位置: 妍妍網 > 碼農

.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,拋棄陳舊的技術。超級硬核,全網無人能及。歡迎加入一起學習,一起進步。

往期精彩回顧

起學習