分享web开发知识

注册/登录|最近发布|今日推荐

主页 IT知识网页技术软件开发前端开发代码编程运营维护技术分享教程案例
当前位置:首页 > 软件开发

.NET ClrProfiler字节码重写实现对应用的跟踪和分析

发布时间:2023-09-06 02:32责任编辑:蔡小小关键词:.NET

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<std::mutex> guard(mapLock); ???????????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<IUnknown> metadata_interfaces; ???????????auto hr = corProfilerInfo->GetModuleMetaData(moduleId, ofRead | ofWrite, ???????????????IID_IMetaDataImport2, ???????????????metadata_interfaces.GetAddressOf()); ???????????RETURN_OK_IF_FAILED(hr); ???????????auto pAssemblyImport = metadata_interfaces.As<IMetaDataAssemblyImport>( ???????????????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重写。我们会对代码修改成如下方式:

public string Test(string a, int? b, int c) ???????{ ???????????object ret = null; ???????????Exception ex = null; ???????????MethodTrace methodTrace = null; ???????????try ???????????{ ???????????????methodTrace= TraceAgent.GetInstance().BeforeMethod("Test", this, new object[] { a, b, c }); ???????????????ret = "1"; ???????????????goto T; ???????????} ???????????catch (Exception e) ???????????{ ???????????????ex = e; ???????????????throw; ???????????} ???????????finally ???????????{ ???????????????if (methodTrace != null) ???????????????{ ???????????????????methodTrace.EndMethod(ret, ex); ???????????????} ???????????} ???????????T: ???????????return (string)ret; ???????}

  

其中主要包含方法本地变量签名重写、方法体字节重写(包含代码体、异常体)。

方法本地变量签名重写代码:  

 ???// add ret ex methodTrace var to local var ???HRESULT ModifyLocalSig(CComPtr<IMetaDataImport2>& pImport, ???????CComPtr<IMetaDataEmit2>& 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操作码全部改为goto到新建的ret指针处(需要判断方法返回类型,进行适当装箱拆箱处理),然后我们新增catch 和finally块字节码,最后我们为原方法新增catch和finall异常处理体。这样我们就实现了整个方法的拦截。

最后看我们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

 

.NET ClrProfiler字节码重写实现对应用的跟踪和分析

原文地址:https://www.cnblogs.com/caozhiyuan/p/10352650.html

知识推荐

我的编程学习网——分享web前端后端开发技术知识。 垃圾信息处理邮箱 tousu563@163.com 网站地图
icp备案号 闽ICP备2023006418号-8 不良信息举报平台 互联网安全管理备案 Copyright 2023 www.wodecom.cn All Rights Reserved