使用C#编写.NET分析器(完结)

NET,分析器,C# · 浏览次数 : 620

小编点评

**代码生成** * 使用 StringBuilder 填充模板内容 * 重命名参数以避免冲突 * 用 `for` 循环枚举参数 * 使用 `switch` switch 処理参数类型 * 使用 `if` else 逻辑处理方法返回值 **性能优化** * 使用 `APM` 等性能分析工具 * 分析代码中的垃圾回收和 JIT 效率 * 使用 `Ref` 类优化方法参数 * 使用 `async` 和 `await` 关键字编写异步方法 * 使用 `Parallel` 类并行执行方法 **示例** **模板** ```csharp sourceBuilder.Replace("{invokerFunctions}", invokerFunctions.ToString()); sourceBuilder.Replace("{invokerName}", invokerName); ``` **性能分析** ```csharp // 使用 APM 分析代码效率 APM.ProfileMethod("sourceMethod"); // 分析代码中的垃圾回收效率 GC.GetObject().ToString(); // 使用 Ref 类优化方法参数 OptimizeMethod(param1, param2); // 使用 async 和 await 关键字编写异步方法 async Task MethodAsync() { // 使用 async 和 await 关键字编写异步方法 return await Method1(); } ```

正文

译者注

这是在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分析器(完结)相似的内容:

使用C#编写.NET分析器(完结)

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

C# 设置PDF表单不可编辑、或提取PDF表单数据

PDF表单是PDF中的可编辑区域,允许用户填写指定信息。当表单填写完成后,有时候我们可能需要将其设置为不可编辑,以保护表单内容的完整性和可靠性。或者需要从PDF表单中提取数据以便后续处理或分析。 之前文章详细介绍过如何使用免费Spire.PDF库通过C# 创建、填写表单,本文将继续介绍该免费.NET

使用C#编写.NET分析器-第二部分

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

使用C#编写.NET分析器(三)

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

4款.NET开源的Redis客户端驱动库

前言 今天给大家推荐4款.NET开源免费的Redis客户端驱动库(以下排名不分先后)。 Redis是什么? Redis全称是REmote DIctionary Service,即远程字典服务。Redis 是一个使用C语言编写的、开源的(遵守 BSD 协议)、支持网络、可基于内存亦可持久化的日志型、K

C#/.NET这些实用的技巧和知识点你都知道吗?

前言 今天大姚给大家分享一些C#/.NET中的实用的技巧和知识点,它们可以帮助我们提升代码质量和编程效率,希望可以帮助到有需要的同学。 .NET使用CsvHelper快速读取和写入CSV文件 本文主要讲解.NET中如何使用CsvHelper这个开源库快速实现CSV文件读取和写入。 https://m

Visual Studio Code安装C#开发工具包并编写ASP.NET Core Web应用

前言 前段时间微软发布了适用于VS Code的C#开发工具包(注意目前该包还属于预发布状态但是可以正常使用),因为之前看过网上的一些使用VS Code搭建.NET Core环境的教程看着还挺复杂的就一直没有尝试使用VS Code来编写.NET Core。不过听说C# 开发工具包提供了一系列功能和扩展

【长文】带你搞明白Redis

Redis,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。 与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每...

看我是如何用C#编写一个小于8KB的贪吃蛇游戏的

译者注:这是Michal Strehovský大佬的一篇文章,他目前在微软.NET Runtime团队工作,主要是负责.NET NativeAOT功能的开发。我在前几天看到这篇文章,非常喜欢,虽然它的内容稍微有点过时(还是使用的.NET Core 3.0),不过其中的一些编程技巧和思维方式很受用,特

c# 如何将程序加密隐藏?

下面将介绍如何通过`LiteDB`将自己的程序进行加密,首先介绍一下`LiteDB`。 ## LiteDB LiteDB是一个轻量级的嵌入式数据库,它是用C#编写的,适用于.NET平台。它的设计目标是提供一个简单易用的数据库解决方案,可以在各种应用程序中使用。 LiteDB使用单个文件作为数据库存储