Demo:https://github.com/caozhiyuan/ClrProfiler.Trace
<https://github.com/caozhiyuan/ClrProfiler.Trace>

背景

为了实现自动、无依赖地跟踪分析应用程序性能(达到商业级APM效果),作者希望能动态修改应用字节码。在相关调研之后,决定采用profiler api进行实现。

介绍

作者将对.NET ClrProfiler 字节码重写技术进行相关阐述。

Profiler
<https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/profiling/profiling-overview>
是微软提供的一套跟踪和分析应用的工具,其提供了一套api可以跟踪和分析.NET程序运行情况。其原理架构图如下:



本文所使用的方式是直接对方法字节码进行重写,动态引用程序集、插入异常捕捉代码、插入执行前后代码。

其中相关基础概念涉及CLI标准(ECMS-355
<https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf>
),CLI标准对公用语言运行时进行了详细的描述。

本文主要涉及到 :

1. 程序集定义、引用

2. 类型定义、引用

3. 方法定义、引用

4. 操作码

5. 签名(此文
<https://www.codeproject.com/Articles/42649/NET-file-format-Signatures-under-the-hood-Part-1>
对签名格式举了很多例子,可以帮助理解)

实现

在此文 <https://www.codeproject.com/Articles/453065/ILRewriting-for-beginners>
中提供了入门级讲解,下面我们直接正题。

在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指针前插入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
<https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf>
<https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf>

Microsoft/clr-samples <https://github.com/Microsoft/clr-samples>

MethodCheck <https://github.com/brian-reichle/MethodCheck>

NET-file-format-Signatures-under-the-hood
<https://www.codeproject.com/Articles/42649/NET-file-format-Signatures-under-the-hood-Part-1>

dd-trace-dotnet <https://github.com/DataDog/dd-trace-dotnet>

 

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信