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

2023-08-31 13:35:20 浏览数 (2)

译者注

这是在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://mp.weixin.qq.com/s/faa9CFD2sEyGdiLMFJnyxw

正文

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

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

代码语言:javascript复制
  1. public unsafe class CorProfilerCallback2 : ICorProfilerCallback2
  2. {
  3. private static readonly Guid ICorProfilerCallback2Guid = Guid.Parse("8a8cc829-ccf2-49fe-bbae-0f022228071a");
  4. private readonly NativeObjects.ICorProfilerCallback2 _corProfilerCallback2;
  5. public CorProfilerCallback2()
  6. {
  7. _corProfilerCallback2 = NativeObjects.ICorProfilerCallback2.Wrap(this);
  8. }
  9. public IntPtr Object => _corProfilerCallback2;
  10. public HResult Initialize(IntPtr pICorProfilerInfoUnk)
  11. {
  12. Console.WriteLine("[Profiler] ICorProfilerCallback2 - Initialize");
  13. // TODO: To be implemented
  14. return HResult.S_OK;
  15. }
  16. public HResult QueryInterface(in Guid guid, out IntPtr ptr)
  17. {
  18. if (guid == ICorProfilerCallback2Guid)
  19. {
  20. Console.WriteLine("[Profiler] ICorProfilerCallback2 - QueryInterface");
  21. ptr = Object;
  22. return HResult.S_OK;
  23. }
  24. ptr = IntPtr.Zero;
  25. return HResult.E_NOTIMPL;
  26. }
  27. // 为了简洁起见,这里省略了接口中所有70多个方法的默认实现。
  28. }

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

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

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

代码语言:javascript复制
  1. public unsafe struct Unknown
  2. {
  3. private readonly IntPtr _self;
  4. public Unknown(IntPtr self)
  5. {
  6. _self = self;
  7. }
  8. private IntPtr* VTable => (IntPtr*)*(IntPtr*)_self;
  9. // TODO: 实现 QueryInterface/AddRef/Release
  10. }

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

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

  1. public HResult QueryInterface(in Guid guid, out IntPtr ptr)
  2. {
  3. var func = (delegate* unmanaged<IntPtr, in Guid, out IntPtr, HResult>)(*VTable);
  4. return func(_self, in guid, out ptr);
  5. }
  6. public int AddRef()
  7. {
  8. var func = (delegate* unmanaged<IntPtr, int>)(*(VTable 1));
  9. return func(_self);
  10. }
  11. public int Release()
  12. {
  13. var func = (delegate* unmanaged<IntPtr, int>)(*(VTable 2));
  14. return func(_self);
  15. }

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

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

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

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

  1. public unsafe struct {invokerName}
  2. {
  3. private readonly IntPtr _self;
  4. public {invokerName}(IntPtr self)
  5. {
  6. _self = self;
  7. }
  8. private IntPtr* VTable => (IntPtr*)*(IntPtr*)_self;
  9. {invokerFunctions}
  10. }

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

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

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

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

  1. var invokerFunctions = new StringBuilder();
  2. var interfaceList = symbol.AllInterfaces.ToList();
  3. interfaceList.Reverse();
  4. interfaceList.Add(symbol);
  5. foreach (var @interface in interfaceList)
  6. {
  7. foreach (var member in @interface.GetMembers())
  8. {
  9. if (member is not IMethodSymbol method)
  10. {
  11. continue;
  12. }
  13. // TODO
  14. }
  15. }

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

  1. invokerFunctions.Append($"public {method.ReturnType} {method.Name}(");
  2. for (int i = 0; i < method.Parameters.Length; i )
  3. {
  4. if (i > 0)
  5. {
  6. invokerFunctions.Append(", ");
  7. }
  8. var refKind = method.Parameters[i].RefKind;
  9. switch (refKind)
  10. {
  11. case RefKind.In:
  12. invokerFunctions.Append("in ");
  13. break;
  14. case RefKind.Out:
  15. invokerFunctions.Append("out ");
  16. break;
  17. case RefKind.Ref:
  18. invokerFunctions.Append("ref ");
  19. break;
  20. }
  21. invokerFunctions.Append($"{method.Parameters[i].Type} a{i}");
  22. }
  23. invokerFunctions.AppendLine(")");

请注意,所有参数均被重命名为 a1、a2、a3...,以避免在原始方法的参数具有奇怪名称时可能发生的冲突。 现在我们可以生成方法的主体,从 vtable中获取方法的地址,并用预期参数调用它:

  1. invokerFunctions.AppendLine("{");
  2. invokerFunctions.Append("var func = (delegate* unmanaged[Stdcall]<IntPtr");
  3. for (int i = 0; i < method.Parameters.Length; i )
  4. {
  5. invokerFunctions.Append(", ");
  6. var refKind = method.Parameters[i].RefKind;
  7. switch (refKind)
  8. {
  9. case RefKind.In:
  10. invokerFunctions.Append("in ");
  11. break;
  12. case RefKind.Out:
  13. invokerFunctions.Append("out ");
  14. break;
  15. case RefKind.Ref:
  16. invokerFunctions.Append("ref ");
  17. break;
  18. }
  19. invokerFunctions.Append(method.Parameters[i].Type);
  20. }
  21. invokerFunctions.AppendLine($", {method.ReturnType}>)*(VTable {delegateCount});");
  22. if (method.ReturnType.SpecialType != SpecialType.System_Void)
  23. {
  24. invokerFunctions.Append("return ");
  25. }
  26. invokerFunctions.Append("func(_self");
  27. for (int i = 0; i < method.Parameters.Length; i )
  28. {
  29. invokerFunctions.Append($", ");
  30. var refKind = method.Parameters[i].RefKind;
  31. switch (refKind)
  32. {
  33. case RefKind.In:
  34. invokerFunctions.Append("in ");
  35. break;
  36. case RefKind.Out:
  37. invokerFunctions.Append("out ");
  38. break;
  39. case RefKind.Ref:
  40. invokerFunctions.Append("ref ");
  41. break;
  42. }
  43. invokerFunctions.Append($"a{i}");
  44. }
  45. invokerFunctions.AppendLine(");");
  46. invokerFunctions.AppendLine("}");

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

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

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

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

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

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

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

0 人点赞