前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用C#编写.NET分析器-第三部分

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

作者头像
InCerry
发布2023-08-31 13:34:37
1980
发布2023-08-31 13:34:37
举报
文章被收录于专栏:InCerry

译者注

这是在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

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

https://mp.weixin.qq.com/s/uZDtrc1py0wvCcUERZnKIw

正文

在第一部分中,我们了解了如何使用 NativeAOT让我们用C#编写一个分析器,以及如何暴露一个伪造的 COM对象来使用分析API。在第二部分中,我们改进了解决方案,使用实例方法替代静态方法。现在我们知道了如何与分析API进行交互,我们将编写一个源代码生成器,自动生成实现 ICorProfilerCallback接口中声明的70多个方法所需的样板代码。

首先,我们需要手动将 ICorProfilerCallback接口转换为C#。从技术上讲,本可以从C++头文件中自动生成这些代码,但是相同的C++代码在C#中可以用不同的方式翻译,因此了解函数的目的以正确语义进行转换十分重要。

JITInlining函数为实际例子。在C++中的原型是:

  1. HRESULT JITInlining(FunctionID callerId, FunctionID calleeId, BOOL *pfShouldInline);

一个简单的C#版本转换可能是:

  1. HResult JITInlining(FunctionId callerId, FunctionId calleeId, in bool pfShouldInline);

但是,如果我们查看函数的文档,我们可以了解到pfShouldInline是一个应由函数自身设置的值。所以我们应该使用out关键字:

  1. Result JITInlining(FunctionId callerId, FunctionId calleeId, out bool pfShouldInline);

在其他情况下,我们会根据意图使用in或ref关键字。这就是为什么我们无法完全自动化这个过程。

在将接口转换为C#之后,我们可以继续创建源代码生成器。请注意,我并不打算编写一个最先进的源代码生成器,主要原因是API非常复杂(是的,这话来自于一个教你如何用C#编写分析器的人),你可以查看Andrew Lock的精彩文章来了解如何编写高级源代码生成器。

编写源代码生成器

要创建源代码生成器,我们在解决方案中添加一个针对 netstandard2.0的类库项目,并添加对 Microsoft.CodeAnalysis.CSharpMicrosoft.CodeAnalysis.Analyzers的引用:

  1. <Project Sdk="Microsoft.NET.Sdk">
  2. <PropertyGroup>
  3. <TargetFramework>netstandard2.0</TargetFramework>
  4. <ImplicitUsings>enable</ImplicitUsings>
  5. <LangVersion>latest</LangVersion>
  6. <IsRoslynComponent>true</IsRoslynComponent>
  7. </PropertyGroup>
  8. <ItemGroup>
  9. <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
  10. <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
  11. <PrivateAssets>all</PrivateAssets>
  12. <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  13. </PackageReference>
  14. </ItemGroup>
  15. </Project>

接下来,我们添加一个实现 ISourceGenerator接口的类,并用 [Generator]属性进行修饰:

  1. [Generator]
  2. public class NativeObjectGenerator : ISourceGenerator
  3. {
  4. public void Initialize(GeneratorInitializationContext context)
  5. {
  6. }
  7. public void Execute(GeneratorExecutionContext context)
  8. {
  9. }
  10. }

我们要做的第一件事是生成一个 [NativeObject]属性。我们将用它来修饰我们想要在源代码生成器上运行的接口。我们使用 RegisterForPostInitialization在管道早期运行这段代码:

  1. [Generator]
  2. public class NativeObjectGenerator : ISourceGenerator
  3. {
  4. public void Initialize(GeneratorInitializationContext context)
  5. {
  6. context.RegisterForPostInitialization(EmitAttribute);
  7. }
  8. public void Execute(GeneratorExecutionContext context)
  9. {
  10. }
  11. private void EmitAttribute(GeneratorPostInitializationContext context)
  12. {
  13. context.AddSource("NativeObjectAttribute.g.cs", """
  14. using System;
  15. [AttributeUsage(AttributeTargets.Interface, Inherited = false, AllowMultiple = false)]
  16. internal class NativeObjectAttribute : Attribute { }
  17. """);
  18. }
  19. }

现在我们需要注册一个 ISyntaxContextReceiver来检查类型并检测哪些类型被我们的 [NativeObject] 属性修饰。

  1. public class SyntaxReceiver : ISyntaxContextReceiver
  2. {
  3. public List<INamedTypeSymbol> Interfaces { get; } = new();
  4. public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
  5. {
  6. if (context.Node is InterfaceDeclarationSyntax classDeclarationSyntax
  7. && classDeclarationSyntax.AttributeLists.Count > 0)
  8. {
  9. var symbol = (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax);
  10. if (symbol.GetAttributes().Any(a => a.AttributeClass.ToDisplayString() == "NativeObjectAttribute"))
  11. {
  12. Interfaces.Add(symbol);
  13. }
  14. }
  15. }
  16. }

基本上,语法接收器将被用于访问语法树中的每个节点。我们检查该节点是否是一个接口声明,如果是,我们检查属性以查找 NativeObjectAttribute。可能有很多事情都可以改进,特别是确认它是否是我们的 NativeObjectAttribute,但我们认为对于我们的目的来说这已经足够好了。

在源代码生成器初始化期间,需要注册语法接收器:

代码语言:javascript
复制
  1. public void Initialize(GeneratorInitializationContext context)
  2. {
  3. context.RegisterForPostInitialization(EmitAttribute);
  4. context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
  5. }

最后,在 Execute方法中,我们获取存储在语法接收器中的接口列表,并为其生成代码:

  1. public void Execute(GeneratorExecutionContext context)
  2. {
  3. if (!(context.SyntaxContextReceiver is SyntaxReceiver receiver))
  4. {
  5. return;
  6. }
  7. foreach (var symbol in receiver.Interfaces)
  8. {
  9. EmitStubForInterface(context, symbol);
  10. }
  11. }

生成Native包装器

对于EmitStubForInterface方法,我们可以使用模板引擎,但是我们将依赖于一个经典的StringBuilder和Replace调用。

首先,我们创建我们的模板:

  1. var sourceBuilder = new StringBuilder("""
  2. using System;
  3. using System.Runtime.InteropServices;
  4. namespace NativeObjects
  5. {
  6. {visibility} unsafe class {typeName} : IDisposable
  7. {
  8. private {typeName}({interfaceName} implementation)
  9. {
  10. const int delegateCount = {delegateCount};
  11. var obj = (IntPtr*)NativeMemory.Alloc((nuint)2 + delegateCount, (nuint)IntPtr.Size);
  12. var vtable = obj + 2;
  13. *obj = (IntPtr)vtable;
  14. var handle = GCHandle.Alloc(implementation);
  15. *(obj + 1) = GCHandle.ToIntPtr(handle);
  16. {functionPointers}
  17. Object = (IntPtr)obj;
  18. }
  19. public IntPtr Object { get; private set; }
  20. public static {typeName} Wrap({interfaceName} implementation) => new(implementation);
  21. public static implicit operator IntPtr({typeName} stub) => stub.Object;
  22. ~{typeName}()
  23. {
  24. Dispose();
  25. }
  26. public void Dispose()
  27. {
  28. if (Object != IntPtr.Zero)
  29. {
  30. NativeMemory.Free((void*)Object);
  31. Object = IntPtr.Zero;
  32. }
  33. GC.SuppressFinalize(this);
  34. }
  35. private static class Exports
  36. {
  37. {exports}
  38. }
  39. }
  40. }
  41. """);

如果你对某些部分不理解,请记得查看前一篇文章。这里唯一的新内容是析构函数和 Dispose方法,我们在其中调用 NativeMemory.Free来释放为该对象分配的内存。接下来,我们需要填充所有的模板部分:{visibility}{typeName}{interfaceName}{delegateCount}{functionPointers}{exports}

首先是简单的部分:

  1. var interfaceName = symbol.ToString();
  2. var typeName = $"{symbol.Name}";
  3. var visibility = symbol.DeclaredAccessibility.ToString().ToLower();
  4. // To be filled later
  5. int delegateCount = 0;
  6. var exports = new StringBuilder();
  7. var functionPointers = new StringBuilder();

对于一个接口 MyProfiler.ICorProfilerCallback,我们将生成一个类型为 NativeObjects.ICorProfilerCallback的包装器。这就是为什么我们将完全限定名存储在 interfaceName(= MyProfiler.ICorProfilerCallback)中,而仅将类型名存储在 typeName(= ICorProfilerCallback)中。

接下来我们想要生成导出列表及其函数指针。我希望源代码生成器支持继承,以避免代码重复,因为 ICorProfilerCallback13实现了 ICorProfilerCallback12,而 ICorProfilerCallback12本身又实现了 ICorProfilerCallback11,依此类推。因此我们提取目标接口继承自的接口列表,并为它们中的每一个提取方法:

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

对于一个 QueryInterface(inGuidguid,outIntPtrptr)方法,我们将生成的导出看起来像这样:

  1. [UnmanagedCallersOnly]
  2. public static int QueryInterface(IntPtr* self, Guid* __arg1, IntPtr* __arg2)
  3. {
  4. var handleAddress = *(self + 1);
  5. var handle = GCHandle.FromIntPtr(handleAddress);
  6. var obj = (IUnknown)handle.Target;
  7. var result = obj.QueryInterface(*__arg1, out var __local2);
  8. *__arg2 = __local2;
  9. return result;
  10. }

由于这些方法是实例方法,我们添加了 IntPtr*self参数。另外,如果托管接口中的函数带有 in/out/ref关键字修饰,我们将参数声明为指针类型,因为 UnmanagedCallersOnly方法不支持 in/out/ref

生成导出所需的代码为:

  1. var parameterList = new StringBuilder();
  2. parameterList.Append("IntPtr* self");
  3. foreach (var parameter in method.Parameters)
  4. {
  5. var isPointer = parameter.RefKind == RefKind.None ? "" : "*";
  6. parameterList.Append($", {parameter.Type}{isPointer} __arg{parameter.Ordinal}");
  7. }
  8. exports.AppendLine($" [UnmanagedCallersOnly]");
  9. exports.AppendLine($" public static {method.ReturnType} {method.Name}({parameterList})");
  10. exports.AppendLine($" {{");
  11. exports.AppendLine($" var handle = GCHandle.FromIntPtr(*(self + 1));");
  12. exports.AppendLine($" var obj = ({interfaceName})handle.Target;");
  13. exports.Append($" ");
  14. if (!method.ReturnsVoid)
  15. {
  16. exports.Append("var result = ");
  17. }
  18. exports.Append($"obj.{method.Name}(");
  19. for (int i = 0; i < method.Parameters.Length; i++)
  20. {
  21. if (i > 0)
  22. {
  23. exports.Append(", ");
  24. }
  25. if (method.Parameters[i].RefKind == RefKind.In)
  26. {
  27. exports.Append($"*__arg{i}");
  28. }
  29. else if (method.Parameters[i].RefKind is RefKind.Out)
  30. {
  31. exports.Append($"out var __local{i}");
  32. }
  33. else
  34. {
  35. exports.Append($"__arg{i}");
  36. }
  37. }
  38. exports.AppendLine(");");
  39. for (int i = 0; i < method.Parameters.Length; i++)
  40. {
  41. if (method.Parameters[i].RefKind is RefKind.Out)
  42. {
  43. exports.AppendLine($" *__arg{i} = __local{i};");
  44. }
  45. }
  46. if (!method.ReturnsVoid)
  47. {
  48. exports.AppendLine($" return result;");
  49. }
  50. exports.AppendLine($" }}");
  51. exports.AppendLine();
  52. exports.AppendLine();

对于函数指针,给定与前面相同的方法,我们希望建立:

  1. *(vtable + 1) = (IntPtr)(delegate* unmanaged<IntPtr*, Guid*, IntPtr*>)&Exports.QueryInterface;

生成代码如下:

  1. var sourceArgsList = new StringBuilder();
  2. sourceArgsList.Append("IntPtr _");
  3. for (int i = 0; i < method.Parameters.Length; i++)
  4. {
  5. sourceArgsList.Append($", {method.Parameters[i].OriginalDefinition} a{i}");
  6. }
  7. functionPointers.Append($" *(vtable + {delegateCount}) = (IntPtr)(delegate* unmanaged<IntPtr*");
  8. for (int i = 0; i < method.Parameters.Length; i++)
  9. {
  10. functionPointers.Append($", {method.Parameters[i].Type}");
  11. if (method.Parameters[i].RefKind != RefKind.None)
  12. {
  13. functionPointers.Append("*");
  14. }
  15. }
  16. if (method.ReturnsVoid)
  17. {
  18. functionPointers.Append(", void");
  19. }
  20. else
  21. {
  22. functionPointers.Append($", {method.ReturnType}");
  23. }
  24. functionPointers.AppendLine($">)&Exports.{method.Name};");
  25. delegateCount++;

我们在接口的每个方法都完成了这个操作后,我们只需替换模板中的值并添加生成的源文件:

  1. sourceBuilder.Replace("{typeName}", typeName);
  2. sourceBuilder.Replace("{visibility}", visibility);
  3. sourceBuilder.Replace("{exports}", exports.ToString());
  4. sourceBuilder.Replace("{interfaceName}", interfaceName);
  5. sourceBuilder.Replace("{delegateCount}", delegateCount.ToString());
  6. sourceBuilder.Replace("{functionPointers}", functionPointers.ToString());
  7. context.AddSource($"{symbol.ContainingNamespace?.Name ?? "_"}.{symbol.Name}.g.cs", sourceBuilder.ToString());

就这样,我们的源代码生成器现在准备好了。

使用生成的代码

要使用我们的源代码生成器,我们可以声明 IUnknownIClassFactoryICorProfilerCallback接口,并用 [NativeObject]属性修饰它们:

  1. [NativeObject]
  2. public interface IUnknown
  3. {
  4. HResult QueryInterface(in Guid guid, out IntPtr ptr);
  5. int AddRef();
  6. int Release();
  7. }

  1. [NativeObject]
  2. internal interface IClassFactory : IUnknown
  3. {
  4. HResult CreateInstance(IntPtr outer, in Guid guid, out IntPtr instance);
  5. HResult LockServer(bool @lock);
  6. }

  1. [NativeObject]
  2. public unsafe interface ICorProfilerCallback : IUnknown
  3. {
  4. HResult Initialize(IntPtr pICorProfilerInfoUnk);
  5. // 70+ 多个方法,在这里省略
  6. }

然后我们实现 IClassFactory并调用 NativeObjects.IClassFactory.Wrap来创建本机包装器并暴露我们的 ICorProfilerCallback实例:

  1. public unsafe class ClassFactory : IClassFactory
  2. {
  3. private NativeObjects.IClassFactory _classFactory;
  4. private CorProfilerCallback2 _corProfilerCallback;
  5. public ClassFactory()
  6. {
  7. _classFactory = NativeObjects.IClassFactory.Wrap(this);
  8. }
  9. // The native wrapper has an implicit cast operator to IntPtr
  10. public IntPtr Object => _classFactory;
  11. public HResult CreateInstance(IntPtr outer, in Guid guid, out IntPtr instance)
  12. {
  13. Console.WriteLine("[Profiler] ClassFactory - CreateInstance");
  14. _corProfilerCallback = new();
  15. instance = _corProfilerCallback.Object;
  16. return HResult.S_OK;
  17. }
  18. public HResult LockServer(bool @lock)
  19. {
  20. return default;
  21. }
  22. public HResult QueryInterface(in Guid guid, out IntPtr ptr)
  23. {
  24. Console.WriteLine("[Profiler] ClassFactory - QueryInterface - " + guid);
  25. if (guid == KnownGuids.ClassFactoryGuid)
  26. {
  27. ptr = Object;
  28. return HResult.S_OK;
  29. }
  30. ptr = IntPtr.Zero;
  31. return HResult.E_NOTIMPL;
  32. }
  33. public int AddRef()
  34. {
  35. return 1; // TODO: 做实际的引用计数
  36. }
  37. public int Release()
  38. {
  39. return 0; // TODO: 做实际的引用计数
  40. }
  41. }

并在 DllGetClassObject中暴露它:

  1. public class DllMain
  2. {
  3. private static ClassFactory Instance;
  4. [UnmanagedCallersOnly(EntryPoint = "DllGetClassObject")]
  5. public static unsafe int DllGetClassObject(void* rclsid, void* riid, nint* ppv)
  6. {
  7. Console.WriteLine("[Profiler] DllGetClassObject");
  8. Instance = new ClassFactory();
  9. *ppv = Instance.Object;
  10. return 0;
  11. }
  12. }

最后,我们可以实现 ICorProfilerCallback的实例:

  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 in next article
  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. // Stripped for brevity: the default implementation of all 70+ methods of the interface
  28. // Automatically generated by the IDE
  29. }

如果我们使用一个测试应用程序运行它,我们会发现这些功能能按预期工作:

  1. [Profiler] DllGetClassObject
  2. [Profiler] ClassFactory - CreateInstance
  3. [Profiler] ICorProfilerCallback2 - QueryInterface
  4. [Profiler] ICorProfilerCallback2 - Initialize
  5. Hello, World!

在下一步中,我们将处理拼图的最后一个缺失部分:实现ICorProfilerCallback.Initialize方法并获取ICorProfilerInfo的实例。这样我们就拥有了与性能分析器API实际交互所需的一切。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-07-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 InCerry 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 译者注
    • 正文
      • 编写源代码生成器
        • 生成Native包装器
          • 使用生成的代码
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档