译者注

这是在Datadog公司任职的Kevin Gosse大佬使用C#编写.NET分析器的系列文章之一,在国内只有很少很少的人了解和研究.NET分析器,它常被用于APM(应用性能诊断)、IDE、诊断工具中,比如Datadog的APM,Visual Studio的分析器以及Rider和Reshaper等等。之前只能使用C++编写,自从.NET NativeAOT发布以后,使用C#编写变为可能。

笔者最近也在尝试开发一个运行时方法注入的工具,欢迎熟悉MSIL 、PE Metadata 布局、CLR 源码、CLR Profiler API的大佬,或者对这个感兴趣的朋友留联系方式或者在公众号留言,一起交流学习。

原作者:Kevin Gosse

原文链接:https://minidump.net/writing-a-net-profiler-in-c-part-3-7d2c59fc017f

项目链接:https://github.com/kevingosse/ManagedDotnetProfiler

使用C#编写.NET分析器-一:https://www.cnblogs.com/InCerry/p/writing-a-net-profiler-in-c-sharp-part-1.html

使用C#编写.NET分析器-二:https://www.cnblogs.com/InCerry/p/writing-a-net-profiler-in-c-sharp-part-2.html

使用C#编写.NET分析器-三:https://www.cnblogs.com/InCerry/p/writing-a-net-profiler-in-c-sharp-part-3.html

正文

在第1部分,我们了解了如何使用NativeAOT让我们用C#编写性能分析器,以及如何暴露一个虚假的COM对象来使用性能分析API。在第2部分,我们完善了方案以使用实例方法而不是静态方法。在第3部分,我们使用源生成器自动化了流程。目前,我们具有暴露ICorProfilerCallback实例所需的一切。然而,为了编写性能分析器,我们还需要能够调用ICorProfilerInfo的方法,这将是本部分的主题。

提醒一下,我们最后得到了以下实现的ICorProfilerCallback:

public unsafe class CorProfilerCallback2 : ICorProfilerCallback2
{
private static readonly Guid ICorProfilerCallback2Guid = Guid.Parse("8a8cc829-ccf2-49fe-bbae-0f022228071a"); private readonly NativeObjects.ICorProfilerCallback2 _corProfilerCallback2; public CorProfilerCallback2()
{
_corProfilerCallback2 = NativeObjects.ICorProfilerCallback2.Wrap(this);
} public IntPtr Object => _corProfilerCallback2; public HResult Initialize(IntPtr pICorProfilerInfoUnk)
{
Console.WriteLine("[Profiler] ICorProfilerCallback2 - Initialize"); // TODO: To be implemented return HResult.S_OK;
} public HResult QueryInterface(in Guid guid, out IntPtr ptr)
{
if (guid == ICorProfilerCallback2Guid)
{
Console.WriteLine("[Profiler] ICorProfilerCallback2 - QueryInterface"); ptr = Object;
return HResult.S_OK;
} ptr = IntPtr.Zero;
return HResult.E_NOTIMPL;
} // 为了简洁起见,这里省略了接口中所有70多个方法的默认实现。
}

当调用Initialize时,我们会收到一个IUnknown的实例。我们需要在其上调用QueryInterface以检索到ICorProfilerInfo的实例。

要将对象暴露给本机代码,我们已经看到如何创建一个虚假的vtable。要使用本地对象,正好相反:我们需要读取它们的vtable以获得方法的地址,然后调用它们。

让我们编写一个包装器,用于从IUnknown的实例中调用方法。因为虚拟对象将其vtable的地址存储为第一个字段,我们只需要读取对象位置处的一个指针即可获得该vtable。我们将这个逻辑提取到我们的包装器的一个属性中,以方便使用:

public unsafe struct Unknown
{
private readonly IntPtr _self; public Unknown(IntPtr self)
{
_self = self;
} private IntPtr* VTable => (IntPtr*)*(IntPtr*)_self; // TODO: 实现 QueryInterface/AddRef/Release
}

注意,我们将该包装器声明为结构(struct),因为它不需要任何状态。最后,这只是一个带有一些嵌入式逻辑的精美指针。

要调用这些方法,我们从vtable的相应槽中检索它们的地址,然后将它们转换为函数指针。然后我们只需要调用它们,确保将对象的地址作为第一个参数传递,因为它们是实例方法:

public HResult QueryInterface(in Guid guid, out IntPtr ptr)
{
var func = (delegate* unmanaged<IntPtr, in Guid, out IntPtr, HResult>)(*VTable); return func(_self, in guid, out ptr);
} public int AddRef()
{
var func = (delegate* unmanaged<IntPtr, int>)(*(VTable + 1)); return func(_self);
} public int Release()
{
var func = (delegate* unmanaged<IntPtr, int>)(*(VTable + 2)); return func(_self);
}

我们的包装器可以直接在ICorProfilerCallback.Initialize中使用,以检索ICorProfilerInfo的实例:

public HResult Initialize(IntPtr pICorProfilerInfoUnk)
{
Console.WriteLine("[Profiler] ICorProfilerCallback2 - Initialize"); var iCorProfilerInfo3Guid = Guid.Parse("B555ED4F-452A-4E54-8B39-B5360BAD32A0"); var unknown = new Unknown(pICorProfilerInfoUnk); var result = unknown.QueryInterface(iCorProfilerInfo3Guid, out var ptr); if (result == HResult.S_OK)
{
Console.WriteLine($"[Profiler] Successfully retrieved an instance of ICorProfilerInfo3: {ptr:x2}");
}
else
{
Console.WriteLine($"[Profiler] Failed with error code: {result:x2}");
} return HResult.S_OK;
}

要实际使用我们的ICorProfilerInfo实例,我们需要编写相同类型的包装器。但是,由于该接口声明了数十个方法,我们不会手动操作,而是将扩展我们在第3部分编写的源代码生成器。

我们的源代码生成器将填充以下模板:

public unsafe struct {invokerName}
{
private readonly IntPtr _self; public {invokerName}(IntPtr self)
{
_self = self;
} private IntPtr* VTable => (IntPtr*)*(IntPtr*)_self; {invokerFunctions}
}

我们将所有这些内容实现在上一篇文章中描述的EmitStubForInterface(GeneratorExecutionContext context, INamedTypeSymbol symbol)方法中。

对于包装器的名称,我们只需使用符号的名称并追加一个后缀:

var invokerName = $"{symbol.Name}Invoker";

然后,我们需要填充函数列表。我们声明一个StringBuilder并开始遍历目标接口及其父接口的所有函数:

var invokerFunctions = new StringBuilder();

var interfaceList = symbol.AllInterfaces.ToList();
interfaceList.Reverse();
interfaceList.Add(symbol); foreach (var @interface in interfaceList)
{
foreach (var member in @interface.GetMembers())
{
if (member is not IMethodSymbol method)
{
continue;
} // TODO
}
}

对于每个方法,我们首先编写签名:

invokerFunctions.Append($"public {method.ReturnType} {method.Name}(");

for (int i = 0; i < method.Parameters.Length; i++)
{
if (i > 0)
{
invokerFunctions.Append(", ");
} var refKind = method.Parameters[i].RefKind; switch (refKind)
{
case RefKind.In:
invokerFunctions.Append("in ");
break;
case RefKind.Out:
invokerFunctions.Append("out ");
break;
case RefKind.Ref:
invokerFunctions.Append("ref ");
break;
} invokerFunctions.Append($"{method.Parameters[i].Type} a{i}");
} invokerFunctions.AppendLine(")");

请注意,所有参数均被重命名为a1、a2、a3...,以避免在原始方法的参数具有奇怪名称时可能发生的冲突。

现在我们可以生成方法的主体,从vtable中获取方法的地址,并用预期参数调用它:

invokerFunctions.AppendLine("{");
invokerFunctions.Append("var func = (delegate* unmanaged[Stdcall]<IntPtr"); for (int i = 0; i < method.Parameters.Length; i++)
{
invokerFunctions.Append(", "); var refKind = method.Parameters[i].RefKind; switch (refKind)
{
case RefKind.In:
invokerFunctions.Append("in ");
break;
case RefKind.Out:
invokerFunctions.Append("out ");
break;
case RefKind.Ref:
invokerFunctions.Append("ref ");
break;
} invokerFunctions.Append(method.Parameters[i].Type);
} invokerFunctions.AppendLine($", {method.ReturnType}>)*(VTable + {delegateCount});"); if (method.ReturnType.SpecialType != SpecialType.System_Void)
{
invokerFunctions.Append("return ");
} invokerFunctions.Append("func(_self"); for (int i = 0; i < method.Parameters.Length; i++)
{
invokerFunctions.Append($", "); var refKind = method.Parameters[i].RefKind; switch (refKind)
{
case RefKind.In:
invokerFunctions.Append("in ");
break;
case RefKind.Out:
invokerFunctions.Append("out ");
break;
case RefKind.Ref:
invokerFunctions.Append("ref ");
break;
} invokerFunctions.Append($"a{i}");
} invokerFunctions.AppendLine(");");
invokerFunctions.AppendLine("}");

这有很多代码,但主要是枚举参数以生成方法调用,以及在方法返回void时进行特殊处理。

最后但同样重要的是,我们替换模板中的占位符:

sourceBuilder.Replace("{invokerFunctions}", invokerFunctions.ToString());
sourceBuilder.Replace("{invokerName}", invokerName);

有了这个,我们可以回到ICorProfilerCallback.Initialize的实现,并用我们自动生成的实现替换Unknown

public HResult Initialize(IntPtr pICorProfilerInfoUnk)
{
Console.WriteLine("[Profiler] ICorProfilerCallback2 - Initialize"); var iCorProfilerInfo3Guid = Guid.Parse("B555ED4F-452A-4E54-8B39-B5360BAD32A0"); var unknown = new NativeObjects.IUnknownInvoker(pICorProfilerInfoUnk); var result = unknown.QueryInterface(iCorProfilerInfo3Guid, out var ptr); if (result == HResult.S_OK)
{
Console.WriteLine($"[Profiler] Successfully retrieved an instance of ICorProfilerInfo3: {ptr:x2}"); var corProfilerInfo = new NativeObjects.ICorProfilerInfo3Invoker(ptr);
// Can start interacting with ICorProfilerInfo
}
else
{
Console.WriteLine($"[Profiler] Failed with error code: {result:x2}");
} return HResult.S_OK;
}

有了这些,我们终于拥有了编写探查器所需的所有拼图碎片。



作为提醒,所有代码均可在GitHub上找到。

.NET性能优化交流群

相信大家在开发中经常会遇到一些性能问题,苦于没有有效的工具去发现性能瓶颈,或者是发现瓶颈以后不知道该如何优化。之前一直有读者朋友询问有没有技术交流群,但是由于各种原因一直都没创建,现在很高兴的在这里宣布,我创建了一个专门交流.NET性能优化经验的群组,主题包括但不限于:

  • 如何找到.NET性能瓶颈,如使用APM、dotnet tools等工具

  • .NET框架底层原理的实现,如垃圾回收器、JIT等等

  • 如何编写高性能的.NET代码,哪些地方存在性能陷阱

希望能有更多志同道合朋友加入,分享一些工作中遇到的.NET性能问题和宝贵的性能分析优化经验。目前一群已满,现在开放二群。

如果提示已经达到200人,可以加我微信,我拉你进群: lishi-wk

另外也创建了QQ群,群号: 687779078,欢迎大家加入。

抽奖送书活动预热!!!

感谢大家对我公众号的支持与陪伴!为庆祝公众号一周年,抽奖送出一些书籍,请大家关注公众号后续推文!

使用C#编写.NET分析器(完结)的更多相关文章

  1. 使用C#编写一个.NET分析器(一)

    译者注 这是在Datadog公司任职的Kevin Gosse大佬使用C#编写.NET分析器的系列文章之一,在国内只有很少很少的人了解和研究.NET分析器,它常被用于APM(应用性能诊断).IDE.诊断 ...

  2. 用 C 语言编写一个简单的垃圾回收器

    人们似乎觉得编写垃圾回收机制是非常难的,是一种仅仅有少数智者和Hans Boehm(et al)才干理解的高深魔法.我觉得编写垃圾回收最难的地方就是内存分配,这和阅读K&R所写的malloc例 ...

  3. python-性能测试

    目录: 1.timeit 1.1 在命令后调用timeit 1.2 在代码中使用 1.3 创建计时器实例,通过autorange获得循环次数 1.4 Wall时间和CPU时间 2.profile和cP ...

  4. atitit.自己动手开发编译器and解释器(2) ------语法分析,语义分析,代码生成--attilax总结

    atitit.自己动手开发编译器and解释器(2) ------语法分析,语义分析,代码生成--attilax总结 1. 建立AST 抽象语法树 Abstract Syntax Tree,AST) 1 ...

  5. .NET Core技术研究-通过Roslyn代码分析技术规范提升代码质量

    随着团队越来越多,越来越大,需求更迭越来越快,每天提交的代码变更由原先的2位数,暴涨到3位数,每天几百次代码Check In,补丁提交,大量的代码审查消耗了大量的资源投入. 如何确保提交代码的质量和提 ...

  6. Flex & Bison 开始

    Flex 与 Bison 是为编译器和解释器的编程人员特别设计的工具: Flex 用于词法分析(lexical analysis,或称 scanning),把输入分割成一个个有意义的词块,称为记号(t ...

  7. (翻译)使用Api分析器与Windows兼容包来编写智能的跨平台.NET Core应用

    本文翻译自Scott Hanselman博客: https://www.hanselman.com/blog/WritingSmarterCrossplatformNETCoreAppsWithThe ...

  8. 《Effective Python:编写高质量Python代码的59个有效方法》读书笔记(完结)

    Effective Python 第1章 用Pythonic方式来思考 be pythonic 遵守pep8 python3有两种字符序列类型:bytes(原始的字节)和str(Unicode字符). ...

  9. C#总结项目《影院售票系统》编写总结完结篇

    回顾:昨天总结了影院售票系统核心部分-售票,整个项目也就完成了2/3了,需求中也要求了对销售信息的保存,今天就继续总结销售信息的保存以及加载销售信息. 分析:退出程序时将已售出票集合中的对象循环写入到 ...

  10. 用C#语言编写:数组分析器

    static void Main(string[] args)        {            #region 创建数组            Console.Write("请输入数 ...

随机推荐

  1. Go Home

    Go Home (https://www.luogu.com.cn/problem/AT_arc070_a) 比较需要理解题意的一个题目 看看题目解析:在0秒的时候有一只袋鼠在左右无限长的数轴上的原点 ...

  2. Tomcat长轮询原理与源码解析

    Tomcat长轮询原理与源码解析 系列文章目录和关于我 零丶长轮询的引入 最近在看工作使用到的diamond配置中心原理,发现大多数配置中心在推和拉模型上做的选择出奇的一致选择了基于长轮询的拉模型 基 ...

  3. java RSA生成公钥和私钥

    1.随机生成密钥对 /** * 随机生成密钥对 * @throws NoSuchAlgorithmException */ public static void genKeyPair() throws ...

  4. 笔记:C++学习之旅---指针

    笔记:C++学习之旅---指针 为什么要使用指针 因为在操作大型数据和类时,由于指针可以通过内存地址直接访问数据,从而避免在程序中赋值大量的代码,因此指针的效率最高,一般来说,指针会有三大用途: 1: ...

  5. vue中实际代码模拟JS中promise调接口的运行流程

    假设我们有一个需要调用接口的场景,我们可以使用Vue中的axios库来发送请求.以下是一个简单的例子: // 引入axios库import axios from 'axios' // 定义一个函数,用 ...

  6. Gusfield算法学习

    算法详解 等价流树正如其名,树上两点间的路径上的边权最小值为图上两点间的最小割. Gusfield算法就是建等价流树的一种算法.设当前正在处理的集合为 \(S(|S|\ge 2)\),从 \(S\) ...

  7. SpringBoot如何自定义一个starter

    SpringBoot starter,大家应该在平常写项目中应该非常熟悉,很多依赖都会提供集成SpringBoot的依赖,这样我们用起来就非常顺手,开箱就能用,那如何自定义一个starter呢? Sp ...

  8. Unity2021SteamingAssets数量限制导致打包异常解决

    最近将项目从Unity2019升级到Unity2021,打包安卓的时候出现异常.查了下资料说是数量超出限制,需要设置不压缩的数量. 1. 使用自定义mainTemplate.gradle 打开编辑,a ...

  9. 【ATL】注册插件成功后自动设置浏览器允许弹出窗口注册表

    STDAPI DllInstall(BOOL bInstall, LPCWSTR pszCmdLine) { HRESULT hr = E_FAIL; static const wchar_t szU ...

  10. Win10系统Anaconda下tensorflow的GPU环境搭建

    我的环境:Win10 + Anaconda + tensorflow-gpu1.14 + CUDA10.0 + cuDNN7.6 + python3.6 注意:tensorflow版本.CUDA版本. ...