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

2023-08-31 13:34:37 浏览数 (1)

译者注

这是在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实际交互所需的一切。

0 人点赞