當前位置: 妍妍網 > 碼農

.NET8反射EntryPoint入口原理

2024-01-31碼農

點選上方 藍字 江湖評談 設為關註




前言

看下透過EntryPoint反射的Man函式入口,是如何被CLR操控的。它本質上跟普通的反射沒有任何區別,些許差距在於托管不同(普通的托管方法反射的是普通的方法,而EntryPoint反射的是托管Main固定入口) 構建反射的要素,第一個就是CLR 獲取到IL 的二進制 程式碼, 第二個就是透過相對應的函式 IL 進制程式碼呼叫JIT進行編譯。 這兩個點是 EntryPoint 呼叫Main的關鍵地方。

例子

非常簡單的例子

staticvoidMain(string[] args){string path = @"E:\Visual Studio Project\Test_\ConsoleApp1\bin\Debug\net8.0\ConsoleApp1.dll";byte[] buffer = File.ReadAllBytes(path); Assembly asm = Assembly.Load(buffer); MethodInfo Point = asm.EntryPoint; Point.Invoke(nullnewobject[] { args });}

ConsoleApp1.dll 的Program.cs程式碼如下:

staticvoidMain(string[] args){ Console.WriteLine("Call Main"); Console.ReadLine();}

EntryPoint

Assembly.Load本身透過buffer緩沖的十六進制字節構建了EntryPoint的入口,它的,而buffer則是讀取path路徑下的托管DLL得來的。Load如下:

publicstatic Assembly Load(byte[] rawAssembly, byte[]? rawSymbolStore){ ArgumentNullException.ThrowIfNull(rawAssembly);if (rawAssembly.Length == 0)thrownew BadImageFormatException(SR.BadImageFormat_BadILFormat); SerializationInfo.ThrowIfDeserializationInProgress("AllowAssembliesFromByteArrays",ref s_cachedSerializationSwitch); AssemblyLoadContext alc = new IndividualAssemblyLoadContext("Assembly.Load(byte[], ...)");return alc.InternalLoad(rawAssembly, rawSymbolStore);}

alc.InternalLoad主要是把托管DLL的二進制和長度傳入到非托管裏面去進行操作,如下:

[RequiresUnreferencedCode("Types and members the loaded assembly depends on might be removed")]internalunsafe Assembly InternalLoad(ReadOnlySpan<byte> arrAssembly, ReadOnlySpan<byte> arrSymbols){ RuntimeAssembly? loadedAssembly = null;fixed (byte* ptrAssembly = arrAssembly, ptrSymbols = arrSymbols) { LoadFromStream(_nativeAssemblyLoadContext, new IntPtr(ptrAssembly), arrAssembly.Length,new IntPtr(ptrSymbols), arrSymbols.Length, ObjectHandleOnStack.Create(ref loadedAssembly)); }return loadedAssembly!;}

LoadFromStream呼叫的是Qcall的AssemblyNative_LoadFromStream

[RequiresUnreferencedCode("Types and members the loaded assembly depends on might be removed")][LibraryImport(RuntimeHelpers.QCall, EntryPoint = "AssemblyNative_LoadFromStream")]privatestaticpartialvoidLoadFromStream(IntPtr ptrNativeAssemblyBinder, IntPtr ptrAssemblyArray, int iAssemblyArrayLen, IntPtr ptrSymbols, int iSymbolArrayLen, ObjectHandleOnStack retAssembly);

AssemblyNative_LoadFromStream主要做了兩件事情,構建程式的IL映像pILImage以及構建托管DLL的程式集pLoadedAssembly,其它會填充一些內部欄位比如EntryPoint,後面會透過asm.EntryPoint直接獲得

extern"C"void QCALLTYPE AssemblyNative_LoadFromStream(INT_PTR ptrNativeAssemblyBinder, INT_PTR ptrAssemblyArray, INT32 cbAssemblyArrayLength, INT_PTR ptrSymbolArray, INT32 cbSymbolArrayLength, QCall::ObjectHandleOnStack retLoadedAssembly){ QCALL_CONTRACT; BEGIN_QCALL;// Ensure that the invariants are in place _ASSERTE(ptrNativeAssemblyBinder != NULL); _ASSERTE((ptrAssemblyArray != NULL) && (cbAssemblyArrayLength > 0)); _ASSERTE((ptrSymbolArray == NULL) || (cbSymbolArrayLength > 0));PEImageHolder pILImage(PEImage::CreateFromByteArray((BYTE*)ptrAssemblyArray, (COUNT_T)cbAssemblyArrayLength));// Need to verify that this is a valid CLR assembly.if (!pILImage->CheckILFormat()) ThrowHR(COR_E_BADIMAGEFORMAT, BFA_BAD_IL);// Get the binder context in which the assembly will be loaded AssemblyBinder *pBinder = reinterpret_cast<AssemblyBinder*>(ptrNativeAssemblyBinder); LoaderAllocator* pLoaderAllocator = pBinder->GetLoaderAllocator();if (pLoaderAllocator && pLoaderAllocator->IsCollectible() && !pILImage->IsILOnly()) {// Loading IJW assemblies into a collectible AssemblyLoadContext is not allowed ThrowHR(COR_E_BADIMAGEFORMAT, BFA_IJW_IN_COLLECTIBLE_ALC); }// Pass the stream based assembly as IL in an attempt to bind and load it Assembly* pLoadedAssembly = AssemblyNative::LoadFromPEImage(pBinder, pILImage); { GCX_COOP(); retLoadedAssembly.Set(pLoadedAssembly->GetExposedObject()); } LOG((LF_ classLOADER, LL_INFO100,"\tLoaded assembly from a file\n"));// In order to assign the PDB image (if present),// the resulting assembly's image needs to be exactly the one// we created above. We need pointer comparison instead of pe image equivalence// to avoid mixed binaries/PDB pairs of other images.// This applies to both Desktop CLR and CoreCLR, with or without fusion. BOOL fIsSameAssembly = (pLoadedAssembly->GetPEAssembly()->GetPEImage() == pILImage);// Setting the PDB info is only applicable for our original assembly.// This applies to both Desktop CLR and CoreCLR, with or without fusion.if (fIsSameAssembly) {#ifdef DEBUGGING_SUPPORTED// If we were given symbols, save a copy of them.if (ptrSymbolArray != NULL) { PBYTE pSymbolArray = reinterpret_cast<PBYTE>(ptrSymbolArray); pLoadedAssembly->GetModule()->SetSymbolBytes(pSymbolArray, (DWORD)cbSymbolArrayLength); }#endif// DEBUGGING_SUPPORTED } END_QCALL;}

這樣的話,就透過傳遞進入的托管DLL二進制裏面定位到了托管Main函式的入口,也即是EntrPoint入口點的所有托管要素。

RuntimeMethodHandle::InvokeMethod

上面程式碼Assembly.Load構建了托管入口的托管要素點,那麽如何呼叫這個托管入口Main函式呢?這是第二步,它的程式碼如下

Point.Invoke(nullnewobject[] { args });

Invoke會呼叫InvokeWithOneArg函式,它會檢查傳遞進來的二進制IL以及呼叫函式InvokeDirectByRefWithFewArgs

internalunsafeobject? InvokeWithOneArg(object? obj, BindingFlags invokeAttr, Binder? binder,object?[] parameters, CultureInfo? culture){ Debug.Assert(_argCount == 1);object? arg = parameters[0];var parametersSpan = new ReadOnlySpan<object?>(in arg);object? copyOfArg = null; Span<object?> copyOfArgs = new(ref copyOfArg);bool copyBack = false; Span<bool> shouldCopyBack = new(ref copyBack);object? ret;if ((_strategy & InvokerStrategy.StrategyDetermined_ObjSpanArgs) == 0) { DetermineStrategy_ObjSpanArgs(ref _strategy, ref _invokeFunc_ObjSpanArgs, _method, _needsByRefStrategy, backwardsCompat: true); } CheckArguments(parametersSpan, copyOfArgs, shouldCopyBack, binder, culture, invokeAttr);if (_invokeFunc_ObjSpanArgs is not null) {try { ret = _invokeFunc_ObjSpanArgs(obj, copyOfArgs); }catch (Exception e) when ((invokeAttr & BindingFlags.DoNotWrapExceptions) == 0) {thrownew TargetInvocationException(e); } }else { ret = InvokeDirectByRefWithFewArgs(obj, copyOfArgs, invokeAttr); } CopyBack(parameters, copyOfArgs, shouldCopyBack);return ret;}

InvokeDirectByRefWithFewArgs主要是填充二進制IL,以及呼叫非托管的RuntimeMethodHandle::InvokeMethod

internalunsafeobject? InvokeDirectByRefWithFewArgs(object? obj, Span<object?> copyOfArgs, BindingFlags invokeAttr){ Debug.Assert(_argCount <= MaxStackAllocArgCount);if ((_strategy & InvokerStrategy.StrategyDetermined_RefArgs) == 0) { DetermineStrategy_RefArgs(ref _strategy, ref _invokeFunc_RefArgs, _method, backwardsCompat: true); } StackAllocatedByRefs byrefs = default;#pragmawarning disable CS8500 IntPtr* pByRefFixedStorage = (IntPtr*)&byrefs;#pragmawarning restore CS8500for (int i = 0; i < _argCount; i++) {#pragmawarning disable CS8500 *(ByReference*)(pByRefFixedStorage + i) = (_invokerArgFlags[i] & InvokerArgFlags.IsValueType) != 0 ?#pragmawarning restore CS8500 ByReference.Create(ref copyOfArgs[i]!.GetRawData()) : ByReference.Create(ref copyOfArgs[i]); }try {return _invokeFunc_RefArgs!(obj, pByRefFixedStorage); }catch (Exception e) when ((invokeAttr & BindingFlags.DoNotWrapExceptions) == 0) {thrownew TargetInvocationException(e); } }

_invokeFunc_RefArgs即是呼叫RuntimeMethodHandle::InvokeMethod

FCIMPL4(Object*, RuntimeMethodHandle::InvokeMethod,Object *target, PVOID* args, // An array of byrefs SignatureNative* pSigUNSAFE, CLR_BOOL fConstructor){ FCALL_CONTRACT;//為方便觀看,此處省略一萬行}

它這裏面主要是呼叫JIT編譯Program.Main函式為機器碼,然後進行執行。那麽整個的流程基本上清晰的展現出來了。

中間部份不重要的函式以及程式碼簡略不提,依然可能有些繁瑣,程式碼較多,但反射EntryPoint的大致基本上如此。

往期精彩回顧