译者注

这是在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. DG:重启之后主备数据重新同步

    问题描述:本来配置好的DG第二天重启之后,发现主备库数据不能同步,在主库上执行日志切换以及创建表操作都传不到备库上,造成这种错误的原因是主库实例断掉后造成备库日志与主库无法实时接收 主库:orcl  ...

  2. [bx] 和 Loop指令

    在masm编译器中不同于debug的命令 如:在debug中 mov ax,[0] --> 是说将 偏移地址为 0 中的数据送入ax中 而在汇编语言中 mov ax,[0] --> 是说将 ...

  3. axios文件下载!!!!

    前端 download(){ debugger; this.loading = true; axios.post('http://localhost:8081/brand_case/dao.do?me ...

  4. Mysql Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column ‘xx‘

    MySql执行分组sql报错: Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated co ...

  5. 2020-09-19:TCP状态有哪些?

    福哥答案2020-09-19:#福大大架构师每日一题# [答案来自此链接](https://www.zhihu.com/question/421833613) 11种状态1.CLOSED状态:初始状态 ...

  6. 2022-07-31:给出一个有n个点,m条有向边的图, 你可以施展魔法,把有向边,变成无向边, 比如A到B的有向边,权重为7。施展魔法之后,A和B通过该边到达彼此的代价都是7。 求,允许施展一次魔法

    2022-07-31:给出一个有n个点,m条有向边的图, 你可以施展魔法,把有向边,变成无向边, 比如A到B的有向边,权重为7.施展魔法之后,A和B通过该边到达彼此的代价都是7. 求,允许施展一次魔法 ...

  7. 2021-11-19:[0,4,7] : 0表示这里石头没有颜色,如果变红代价是4,如果变蓝代价是7,[1,X,X] : 1表示这里石头已经是红,而且不能改颜色,所以后两个数X无意义,[2,X,X]

    2021-11-19:[0,4,7] : 0表示这里石头没有颜色,如果变红代价是4,如果变蓝代价是7,[1,X,X] : 1表示这里石头已经是红,而且不能改颜色,所以后两个数X无意义,[2,X,X] ...

  8. qq飞车端游最全按键指法教学

    目录 起步篇 超级起步 弹射起步 段位起步 基础篇 点飘 撞墙漂移 撞墙点喷 进阶篇 双喷 叠喷 断位漂移 段位双喷 侧身漂移 快速出弯 CW WCW CWW 牵引 甩尾点飘 甩尾漂移 右侧卡 左侧卡 ...

  9. Docker安装MS SQL Server并使用Navicat远程连接

    MS SQL Server简介 Microsoft SQL Server(简称SQL Server)是由微软公司开发的关系数据库管理系统,它是一个功能强大.性能卓越的企业级数据库平台,用于存储和处理大 ...

  10. 【技术积累】Python中的PyTorch库【一】

    PyTorch库介绍 PyTorch是一个基于Python的科学计算库,用于构建深度学习神经网络.它主要由两个部分组成:一个是PyTorch Tensor库,提供了类似于NumPy的数组操作,但是支持 ...