點選上方 藍字 江湖評談 設為關註
前言
看下透過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(null, newobject[] { 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(null, newobject[] { 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 CS8500
for (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的大致基本上如此。
往期精彩回顧