Demo:https://github.com/caozhiyuan/ClrProfiler.Trace
背景
為了實現自動、無依賴地跟蹤分析應用程式效能(達到商業級APM效果),作者希望能動態修改應用位元組碼。在相關調研之後,決定採用profiler api進行實現。
介紹
作者將對.NET ClrProfiler 位元組碼重寫技術進行相關闡述。
Profiler是微軟提供的一套跟蹤和分析應用的工具,其提供了一套api可以跟蹤和分析.NET程式執行情況。其原理架構圖如下:
本文所使用的方式是直接對方法位元組碼進行重寫,動態取用程式集、插入異常捕捉程式碼、插入執行前後程式碼。
其中相關基礎概念涉及CLI標準(ECMS-355),CLI標準對公用語言執行時進行了詳細的描述。
本文主要涉及到 :
1. 程式集定義、取用
2. 型別定義、取用
3. 方法定義、取用
4. 操作碼
5. 簽名(此文對簽名格式舉了很多例子,可以幫助理解)
實現
在此文中提供了入門級講解,下麵我們直接正題。
在JIt編譯時候將會對CorProfiler類進行初始化,在此環節我們主要對於監聽的事件進行訂閱和配置初始化工作,我們主要關心ModuleLoad事件。
HRESULT STDMETHODCALLTYPE CorProfiler::Initialize(IUnknown *pICorProfilerInfoUnk) { const HRESULT queryHR = pICorProfilerInfoUnk->QueryInterface(__uuidof(ICorProfilerInfo8), reinterpret_cast < void **>(& this ->corProfilerInfo)); if (FAILED(queryHR)) { return E_FAIL; } const DWORD eventMask = COR_PRF_MONITOR_JIT_COMPILATION | COR_PRF_DISABLE_TRANSPARENCY_CHECKS_UNDER_FULL_TRUST | /* helps the case where this profiler is used on Full CLR */ COR_PRF_DISABLE_INLINING | COR_PRF_MONITOR_MODULE_LOADS | COR_PRF_DISABLE_ALL_NGEN_IMAGES; this ->corProfilerInfo->SetEventMask(eventMask); this ->clrProfilerHomeEnvValue = GetEnvironmentValue(ClrProfilerHome); if ( this ->clrProfilerHomeEnvValue.empty()) { Warn( "ClrProfilerHome Not Found" ); return E_FAIL; } this ->traceConfig = LoadTraceConfig( this ->clrProfilerHomeEnvValue); if ( this ->traceConfig.traceAssemblies.empty()) { Warn( "TraceAssemblies Not Found" ); return E_FAIL; } Info( "CorProfiler Initialize Success" ); return S_OK; } |
在ModuleLoadFinished後,我們主要獲取程式集的EntryPointToken(mian方法token)、執行時mscorlib.dll(net framework)或System.Private.CoreLib.dll(netcore)程式版本基礎資訊以供後面動態取用。
HRESULT STDMETHODCALLTYPE CorProfiler::ModuleLoadFinished(ModuleID moduleId, HRESULT hrStatus) { auto module_info = GetModuleInfo( this ->corProfilerInfo, moduleId); if (!module_info.IsValid() || module_info.IsWindowsRuntime()) { return S_OK; } if (module_info.assembly.name == "dotnet" _W || module_info.assembly.name == "MSBuild" _W) { return S_OK; } const auto entryPointToken = module_info.GetEntryPointToken(); ModuleMetaInfo* module_metadata = new ModuleMetaInfo(entryPointToken, module_info.assembly.name); { std::lock_guard<:mutex> guard(mapLock);</:mutex> moduleMetaInfoMap[moduleId] = module_metadata; } if (entryPointToken != mdTokenNil) { Info( "Assembly:{} EntryPointToken:{}" , ToString(module_info.assembly.name), entryPointToken); } if (module_info.assembly.name == "mscorlib" _W || module_info.assembly.name == "System.Private.CoreLib" _W) { if (!corAssemblyProperty.szName.empty()) { return S_OK; } CComPtr metadata_interfaces; auto hr = corProfilerInfo->GetModuleMetaData(moduleId, ofRead | ofWrite, IID_IMetaDataImport2, metadata_interfaces.GetAddressOf()); RETURN_OK_IF_FAILED(hr); auto pAssemblyImport = metadata_interfaces.As( IID_IMetaDataAssemblyImport); if (pAssemblyImport.IsNull()) { return S_OK; } mdAssembly assembly; hr = pAssemblyImport->GetAssemblyFromScope(&assembly;); RETURN_OK_IF_FAILED(hr); hr = pAssemblyImport->GetAssemblyProps( assembly, &corAssemblyProperty.ppbPublicKey;, &corAssemblyProperty.pcbPublicKey;, &corAssemblyProperty.pulHashAlgId;, NULL, 0, NULL, &corAssemblyProperty.pMetaData;, &corAssemblyProperty.assemblyFlags;); RETURN_OK_IF_FAILED(hr); corAssemblyProperty.szName = module_info.assembly.name; return S_OK; } return S_OK; } |
下麵進行方法編譯,在JITCompilationStarted時,我們會進行Main方法位元組碼插入動態載入Trace程式集(Main方法前新增Assembly.LoadFrom(path))。
在指定方法編譯時,我們需要對方法簽名進行分析,方法簽名中主要包含方法呼叫方式、引數個數、泛型引數個數、傳回型別、引數型別集合。
在分析完方法簽名和方法名後與我們配置的方法進行匹配,如果一致進行IL重寫。我們會對程式碼修改成如下方式:
private Task DataRead( string a, int b) { return Task.Delay(10); } private Task DataReadWrapper( string a, int b) { object ret = null ; Exception ex = null ; MethodTrace methodTrace = null ; try { methodTrace = (MethodTrace) ((TraceAgent) TraceAgent.GetInstance()) .BeforeMethod( this .GetType(), this , new object [] {a, b}, functiontoken); ret = Task.Delay(10); goto T; } catch (Exception e) { ex = e; throw ; } finally { if (methodTrace != null ) { methodTrace.EndMethod(ret, ex); } } T: return (Task)ret; } |
其中主要包含方法本地變數簽名重寫、方法體位元組重寫(包含程式碼體、異常體)。
方法本地變數簽名重寫程式碼:
// add ret ex methodTrace var to local var HRESULT ModifyLocalSig(CComPtr& pImport, CComPtr& pEmit, ILRewriter& reWriter, mdTypeRef exTypeRef, mdTypeRef methodTraceTypeRef) { HRESULT hr; PCCOR_SIGNATURE rgbOrigSig = NULL; ULONG cbOrigSig = 0; UNALIGNED INT32 temp = 0; if (reWriter.m_tkLocalVarSig != mdTokenNil) { IfFailRet(pImport->GetSigFromToken(reWriter.m_tkLocalVarSig, &rgbOrigSig;, &cbOrigSig;)); //Check Is ReWrite or not const auto len = CorSigCompressToken(methodTraceTypeRef, &temp;); if (cbOrigSig - len > 0){ if (rgbOrigSig[cbOrigSig - len -1]== ELEMENT_TYPE_CLASS){ if ( memcmp (&rgbOrigSig;[cbOrigSig - len], &temp;, len) == 0) { return E_FAIL; } } } } auto exTypeRefSize = CorSigCompressToken(exTypeRef, &temp;); auto methodTraceTypeRefSize = CorSigCompressToken(methodTraceTypeRef, &temp;); ULONG cbNewSize = cbOrigSig + 1 + 1 + methodTraceTypeRefSize + 1 + exTypeRefSize; ULONG cOrigLocals; ULONG cNewLocalsLen; ULONG cbOrigLocals = 0; if (cbOrigSig == 0) { cbNewSize += 2; reWriter.cNewLocals = 3; cNewLocalsLen = CorSigCompressData(reWriter.cNewLocals, &temp;); } else { cbOrigLocals = CorSigUncompressData(rgbOrigSig + 1, &cOrigLocals;); reWriter.cNewLocals = cOrigLocals + 3; cNewLocalsLen = CorSigCompressData(reWriter.cNewLocals, &temp;); cbNewSize += cNewLocalsLen - cbOrigLocals; } const auto rgbNewSig = new COR_SIGNATURE[cbNewSize]; *rgbNewSig = IMAGE_CEE_CS_CALLCONV_LOCAL_SIG; ULONG rgbNewSigOffset = 1; memcpy (rgbNewSig + rgbNewSigOffset, &temp;, cNewLocalsLen); rgbNewSigOffset += cNewLocalsLen; if (cbOrigSig > 0) { const auto cbOrigCopyLen = cbOrigSig - 1 - cbOrigLocals; memcpy (rgbNewSig + rgbNewSigOffset, rgbOrigSig + 1 + cbOrigLocals, cbOrigCopyLen); rgbNewSigOffset += cbOrigCopyLen; } rgbNewSig[rgbNewSigOffset++] = ELEMENT_TYPE_OBJECT; rgbNewSig[rgbNewSigOffset++] = ELEMENT_TYPE_CLASS; exTypeRefSize = CorSigCompressToken(exTypeRef, &temp;); memcpy (rgbNewSig + rgbNewSigOffset, &temp;, exTypeRefSize); rgbNewSigOffset += exTypeRefSize; rgbNewSig[rgbNewSigOffset++] = ELEMENT_TYPE_CLASS; methodTraceTypeRefSize = CorSigCompressToken(methodTraceTypeRef, &temp;); memcpy (rgbNewSig + rgbNewSigOffset, &temp;, methodTraceTypeRefSize); rgbNewSigOffset += methodTraceTypeRefSize; IfFailRet(pEmit->GetTokenFromSig(&rgbNewSig;[0], cbNewSize, &reWriter.m;_tkLocalVarSig)); return S_OK; } |
方法體重寫主要涉及到如下資料結構:
struct ILInstr { ILInstr* m_pNext; ILInstr* m_pPrev; unsigned m_opcode; unsigned m_offset; union { ILInstr* m_pTarget; INT8 m_Arg8; INT16 m_Arg16; INT32 m_Arg32; INT64 m_Arg64; }; }; struct EHClause { CorExceptionFlag m_Flags; ILInstr* m_pTryBegin; ILInstr* m_pTryEnd; ILInstr* m_pHandlerBegin; // First instruction inside the handler ILInstr* m_pHandlerEnd; // Last instruction inside the handler union { DWORD m_ClassToken; // use for type-based exception handlers ILInstr* m_pFilter; // use for filter-based exception handlers // (COR_ILEXCEPTION_CLAUSE_FILTER is set) }; }; |
il_rewriter.cpp會將方法體位元組解析成一個雙向連結串列,便於我們在連結串列中插入位元組碼。我們在方法頭指標前插入pre執行程式碼,同時新建一個ret指標,在ret指標前插入catch 和finally塊位元組碼(需要判斷方法傳回型別,進行適當拆箱處理),原ret操作碼全部改為goto到新建的endfinally指標next處,最後我們為原方法新增catch和finally異常處理體。這樣我們就實現了整個方法的攔截。
最後看我們TraceAgent程式碼實現,我們透過Type和functiontoken獲取到MethodBase,然後透過配置獲取標的跟蹤程式集實現對方法的跟蹤和分析。
public EndMethodDelegate BeforeWrappedMethod( object type, object invocationTarget, object [] methodArguments, uint functionToken) { if (invocationTarget == null ) { throw new ArgumentException(nameof(invocationTarget)); } var traceMethodInfo = new TraceMethodInfo { InvocationTarget = invocationTarget, MethodArguments = methodArguments, Type = (Type) type }; var functionInfo = GetFunctionInfoFromCache(functionToken, traceMethodInfo); traceMethodInfo.MethodBase = functionInfo.MethodBase; if (functionInfo.MethodWrapper == null ) { PrepareMethodWrapper(functionInfo, traceMethodInfo); } return functionInfo.MethodWrapper?.BeforeWrappedMethod(traceMethodInfo); } |
結論
透過Profiler API我們動態實現了.NET應用的跟蹤和分析,並且只要配置環境變數(profiler.dll目錄等)。與傳統的dynamicproxy或手動埋點相比,其更加靈活,且無依賴。
參考
ECMA-ST/ECMA-335.pdf
Microsoft/clr-samples
MethodCheck
NET-file-format-Signatures-under-the-hood
dd-trace-dotnet
原文地址:https://www.cnblogs.com/caozhiyuan/p/10352650.html
朋友會在“發現-看一看”看到你“在看”的內容