• .NET ClrProfiler ILRewrite 商业级APM原理


    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重写。我们会对代码修改成如下方式:

            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<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指针前插入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

     

  • 相关阅读:
    MapReduce编程
    Xcode7 真机调试
    【学习笔记】【OC语言】NSString
    【学习笔记】【OC语言】多态
    【学习笔记】【OC语言】继承
    【学习笔记】【OC语言】self关键字
    【学习笔记】【OC语言】类方法
    【学习笔记】【OC语言】set方法和get方法
    【学习笔记】【OC语言】创建对象
    【学习笔记】【OC语言】面向对象思想
  • 原文地址:https://www.cnblogs.com/caozhiyuan/p/10352650.html
Copyright © 2020-2023  润新知