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

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

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

时间: 2024-11-13 10:14:16

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

[WebKit内核] JavaScript引擎深度解析--基础篇(一)字节码生成及语法树的构建详情分析

[WebKit内核] JavaScript引擎深度解析--基础篇(一)字节码生成及语法树的构建详情分析 标签: webkit内核JavaScriptCore 2015-03-26 23:26 2285人阅读 评论(1) 收藏 举报  分类: Webkit(34)  JavascriptCore/JIT(3)  版权声明:本文为博主原创文章,未经博主允许不得转载. 看到HorkeyChen写的文章<[WebKit] JavaScriptCore解析--基础篇(三)从脚本代码到JIT编译的代码实现>

从字节码指令看重写在JVM中的实现

Java是解释执行的,包括动态链接的特性,都给解析或运行期间提供了很多灵活扩展的空间.面向对象语言的继承.封装和多态的特性,在JVM中是怎样进行编译.解析,以及通过字节码指令如何确定方法调用的版本是本文如下要探讨的主要内容,全文围绕一个多态的简单举例来看在JVM中是如何实现的. 先简单介绍几个概念.对于字节码执行模型及字节码指令集的相关概念可以参考之前的一篇介绍http://blog.csdn.net/lijingyao8206/article/details/46562933. 一.方法调用的

[jvm解析系列][十二]分派,重载和重写,查看字节码带你深入了解分派的过程。

重载和重写是分派中的两个重要体现,也是因为这个原因我们才把重载和重写写在了标题上.这一章我们的很多部分都在代码试验上. 总的来说分派分为静态分派和动态分派两种. 静态分派: 首先我们来看一段源码: public class Dispatch { public static void main(String[] args){ Animal a = new Dog(); Animal b = new Cat(); sound(a); sound(b); } public static void so

java类文件结构(字节码文件)

[0]README 0.1)本文部分文字描述转自 "深入理解jvm",旨在学习类文件结构  的基础知识: 0.2)本文荔枝以及荔枝的分析均为原创: 0.3)下面的截图中有附注t*编号,不关乎博文内容: [1]类文件概述 1)各种不同平台的虚拟机与所有平台都统一使用存储格式--字节码,他是构成平台无关性的基石: 2)时至今日,商业机构和开源机构已经在 java语言外发展出一大批在 jvm 上运行的语言,如 Groovy, JRuby, Jython,Scala等: 3)实现语言无关性的基

从字节码和JVM的角度解析Java核心类String的不可变特性

1. 前言 最近看到几个有趣的关于Java核心类String的问题. String类是如何实现其不可变的特性的,设计成不可变的好处在哪里. 为什么不推荐使用+号的方式去形成新的字符串,推荐使用StringBuilder或者StringBuffer呢. 翻阅了网上的一些博客和stackoverflow,结合自己的理解做一个汇总. 2. String类是如何实现不可变的 String类的一大特点,就是使用Final类修饰符. A class can be declared final if its

实例详解:反编译Android APK,修改字节码后再回编译成APK

本文详细介绍了如何反编译一个未被混淆过的Android APK,修改smali字节码后,再回编译成APK并更新签名,使之可正常安装.破译后的apk无论输入什么样的用户名和密码都可以成功进入到第二个Activity. 有时难免要反编译一个APK,修改其中的若干关键判断点,然后再回编译成一个全新的可用的apk,这完全是可实现的.若要完成上述工作,需要以下工具,杂家后面会把下载链接也附上.这些软件截止本文发布时,经过杂家确认都是最新的版本. 1.APK-Multi-Toolv1.0.11.zip 用它

008 虚拟机字节码执行引擎

执行引擎是Java虚拟机最核心的组成部分之一. 物理机的执行引擎建立在处理器.硬件.指令集和操作系统之上的,虚拟机的执行引擎需要自己实现,因此可以自己制定指令集与执行引擎的结构体系,并且支持那些不被硬件直接支持的指令集格式. 1.运行时栈帧结构 栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区域的虚拟机栈的栈元素. 栈帧存储了方法的局部变量表.操作数栈.动态连接和方法返回地址. 对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,成为当前栈帧(Current

实例具体解释:反编译Android APK,改动字节码后再回编译成APK

本文具体介绍了怎样反编译一个未被混淆过的Android APK,改动smali字节码后,再回编译成APK并更新签名,使之可正常安装.破译后的apk不管输入什么样的username和password都能够成功进入到第二个Activity. 有时难免要反编译一个APK.改动当中的若干关键推断点,然后再回编译成一个全新的可用的apk,这全然是可实现的. 若要完毕上述工作,须要以下工具,杂家后面会把下载链接也附上.这些软件截止本文公布时,经过杂家确认都是最新的版本号. 1.APK-Multi-Toolv

虚拟机字节码执行引擎

在前面的几篇文章里,从Java虚拟机内存结构开始,经历了虚拟机垃圾收集机制.Class类文件结构到后来的虚拟机类加载机制,一步步的进入到了Java虚拟机即Java底层的世界.在有了前面的基础之后,接下来就应该进入Java虚拟机最重要的部分了--虚拟机字节码执行引擎,毕竟,这是Java程序得以在不同机器上运行的核心部分. Java是通过实现Java虚拟机来达到平台无关的."虚拟机"的概念是相对于"物理机"来说的,两种机器都有执行代码的能力,不过物理机是直接面向处理器.