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

.NET ClrProfiler ILRewrite 商业级APM原理的更多相关文章

  1. APM 原理与框架选型

    发些存稿:) 0. APM简介 随着微服务架构的流行,一次请求往往需要涉及到多个服务,因此服务性能监控和排查就变得更复杂: 不同的服务可能由不同的团队开发.甚至可能使用不同的编程语言来实现 服务有可能 ...

  2. Dotnet全平台下APM-Trace探索

    背景 随着支撑的内部业务系统越来越多,向着服务化架构进化,在整个迭代过程中,会逐渐暴露出以下问题. 传统依赖于应用服务器日志等手段的排除故障原因的复杂度越来越高,传统的监控服务已经无法满足需求. 终端 ...

  3. APM技术原理

    链接地址:http://www.infoq.com/cn/articles/apm-Pinpoint-practice 1.什么是APM? APM,全称:Application Performance ...

  4. APM之原理篇

    APM,应用性能监控,有new relic等产品,对APM感兴趣的应该不会不知道它了.主要功能就是统计分析应用的CPU.内存.网络.数据库.UI等性能,并提供错误日志捕获.编码人员需要做的仅仅是使用它 ...

  5. 【干货】解密监控宝Docker监控实现原理

    分享人高驰涛(Neeke),云智慧高级架构师,PHP 开发组成员,同时也是 PECL/SeasLog 的作者.8 年研发管理经验,早期从事大规模企业信息化研发架构,09 年涉足互联网数字营销领域并深入 ...

  6. C#多线程之旅(4)——APM初探

    源码地址:https://github.com/Jackson0714/Threads 原文地址:C#多线程之旅(4)——APM初探 C#多线程之旅目录: C#多线程之旅(1)——介绍和基本概念 C# ...

  7. Atitit.并发编程原理与概论 attilax总结

    Atitit.并发编程原理与概论 attilax总结 1. 并发一般涉及如下几个方面:2 2. 线程安全性 ( 2.2 原子性 2.3 加锁机制2 2.1. 线程封闭3.3.1Ad-hoc线程封闭 3 ...

  8. MapReduce原理及其主要实现平台分析

    原文:http://www.infotech.ac.cn/article/2012/1003-3513-28-2-60.html MapReduce原理及其主要实现平台分析 亢丽芸, 王效岳, 白如江 ...

  9. kafka原理和架构

    转载自:  https://blog.csdn.net/lp284558195/article/details/80297208 参考:   https://blog.csdn.net/qq_2059 ...

随机推荐

  1. laravel5.4 后台RBAC功能完成中遇到的问题及解决方法

    1.在后台模块中有些公共的地方 比如头部 尾部 左侧菜单栏; 在laravel中通过继承模板来实现,但是在做RBAC的时候 需求是:不同的登陆用户显示不同的菜单;去数据库获取这些数据 但是每个界面都要 ...

  2. jdbc 增删改查以及遇见的 数据库报错Can't get hostname for your address如何解决

    最近开始复习以前学过的JDBC今天肝了一晚上 来睡睡回笼觉,长话短说 我们现在开始. 我们先写一个获取数据库连接的jdbc封装类 以后可以用 如果不是maven环境的话在src文件下新建一个db.pr ...

  3. 计算机17-1,2作业D

    D.环形矩阵 Description 给定一个整数m,按m形成一个环形矩阵.如m=5,则环形矩阵为: 1   1   1   1   1   1   1   1   1    1   2   2   ...

  4. css3 深入理解flex布局

    一.简要介绍 css3最喜欢的新属性之一便是flex布局属性,用六个字概括便是简单.方便.快速. flex( flexible box:弹性布局盒模型),是2009年w3c提出的一种可以简洁.快速弹性 ...

  5. 隐马尔可夫模型(HMM)总结

    摘要: 1.算法概述 2.算法推导 3.算法特性及优缺点 4.注意事项(算法过程,调参等注意事项) 5.实现和具体例子 6.适用场合 内容: 1.算法概述 隐马尔科夫模型(Hidden Markov ...

  6. 安卓开发笔记(三十一):shape标签下子类根结点的具体使用

    在我的上一篇博文当中阐述了我们如何使用shape标签进行自定义控件,这里对shape控件的属性进行阐述,不知道如何使用这些属性的可以参见我的上一篇博文(自定义Button):https://www.c ...

  7. ASP.NET Core的实时库: SignalR简介及使用

    大纲 本系列会分为2-3篇文章. 第一篇介绍了SignalR的预备知识和原理 本文介绍SignalR以及ASP.NET Core里使用SignalR. 本文的内容: 介绍SignalR 在ASP.NE ...

  8. 滴滴 App 的质量优化框架 Booster,开源了!

    一. 序 当 App 达到一定体量的时候,肯定是要考虑质量优化.有些小问题,看似只有 0.01% 触发率,但是如果发生在 DAU 过千万的产品中,就很严重了. 滴滴这个独角兽的 DAU 早已过千万,自 ...

  9. ie兼容问题记录

    工作中遇到的ie网站兼容性问题  头疼.......... 以下为从网上搜索学习的整理兼容性方法 用于自己记录 #兼容问题 ##css hack: https://blog.csdn.net/fres ...

  10. Spark学习之RDD编程总结

    Spark 对数据的核心抽象——弹性分布式数据集(Resilient Distributed Dataset,简称 RDD).RDD 其实就是分布式的元素集合.在 Spark 中,对数据的所有操作不外 ...