歡迎光臨
每天分享高質量文章

.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<: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(), thisnew 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

已同步到看一看
贊(0)

分享創造快樂