- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
这是在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 。
在第一部分中,我们了解了如何使用 NativeAOT 让我们用C#编写一个分析器,以及如何暴露一个伪造的 COM 对象来使用分析API。在第二部分中,我们改进了解决方案,使用实例方法替代静态方法。现在我们知道了如何与分析API进行交互,我们将编写一个源代码生成器,自动生成实现 ICorProfilerCallback 接口中声明的70多个方法所需的样板代码.
首先,我们需要手动将 ICorProfilerCallback 接口转换为C#。从技术上讲,本可以从C++头文件中自动生成这些代码,但是相同的C++代码在C#中可以用不同的方式翻译,因此了解函数的目的以正确语义进行转换十分重要.
以 JITInlining 函数为实际例子。在C++中的原型是:
HRESULT JITInlining(FunctionID callerId, FunctionID calleeId, BOOL *pfShouldInline);
一个简单的C#版本转换可能是:
HResult JITInlining(FunctionId callerId, FunctionId calleeId, in bool pfShouldInline);
但是,如果我们查看函数的文档,我们可以了解到pfShouldInline是一个应由函数自身设置的值。所以我们应该使用out关键字:
Result JITInlining(FunctionId callerId, FunctionId calleeId, out bool pfShouldInline);
在其他情况下,我们会根据意图使用in或ref关键字。这就是为什么我们无法完全自动化这个过程.
在将接口转换为C#之后,我们可以继续创建源代码生成器。请注意,我并不打算编写一个最先进的源代码生成器,主要原因是API非常复杂(是的,这话来自于一个教你如何用C#编写分析器的人),你可以查看Andrew Lock的精彩文章来了解如何编写高级源代码生成器.
要创建源代码生成器,我们在解决方案中添加一个针对 netstandard2.0 的类库项目,并添加对 Microsoft.CodeAnalysis.CSharp 和 Microsoft.CodeAnalysis.Analyzers 的引用:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
接下来,我们添加一个实现 ISourceGenerator 接口的类,并用 [Generator] 属性进行修饰:
[Generator]
public class NativeObjectGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
}
public void Execute(GeneratorExecutionContext context)
{
}
}
我们要做的第一件事是生成一个 [NativeObject] 属性。我们将用它来修饰我们想要在源代码生成器上运行的接口。我们使用 RegisterForPostInitialization 在管道早期运行这段代码:
[Generator]
public class NativeObjectGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForPostInitialization(EmitAttribute);
}
public void Execute(GeneratorExecutionContext context)
{
}
private void EmitAttribute(GeneratorPostInitializationContext context)
{
context.AddSource("NativeObjectAttribute.g.cs", """
using System;
[AttributeUsage(AttributeTargets.Interface, Inherited = false, AllowMultiple = false)]
internal class NativeObjectAttribute : Attribute { }
""");
}
}
现在我们需要注册一个 ISyntaxContextReceiver 来检查类型并检测哪些类型被我们的 [NativeObject] 属性修饰.
public class SyntaxReceiver : ISyntaxContextReceiver
{
public List<INamedTypeSymbol> Interfaces { get; } = new();
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (context.Node is InterfaceDeclarationSyntax classDeclarationSyntax
&& classDeclarationSyntax.AttributeLists.Count > 0)
{
var symbol = (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax);
if (symbol.GetAttributes().Any(a => a.AttributeClass.ToDisplayString() == "NativeObjectAttribute"))
{
Interfaces.Add(symbol);
}
}
}
}
基本上,语法接收器将被用于访问语法树中的每个节点。我们检查该节点是否是一个接口声明,如果是,我们检查属性以查找 NativeObjectAttribute 。可能有很多事情都可以改进,特别是确认它是否是我们的 NativeObjectAttribute ,但我们认为对于我们的目的来说这已经足够好了.
在源代码生成器初始化期间,需要注册语法接收器:
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForPostInitialization(EmitAttribute);
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
最后,在 Execute 方法中,我们获取存储在语法接收器中的接口列表,并为其生成代码:
public void Execute(GeneratorExecutionContext context)
{
if (!(context.SyntaxContextReceiver is SyntaxReceiver receiver))
{
return;
}
foreach (var symbol in receiver.Interfaces)
{
EmitStubForInterface(context, symbol);
}
}
对于EmitStubForInterface方法,我们可以使用模板引擎,但是我们将依赖于一个经典的StringBuilder和Replace调用.
首先,我们创建我们的模板
var sourceBuilder = new StringBuilder("""
using System;
using System.Runtime.InteropServices;
namespace NativeObjects
{
{visibility} unsafe class {typeName} : IDisposable
{
private {typeName}({interfaceName} implementation)
{
const int delegateCount = {delegateCount};
var obj = (IntPtr*)NativeMemory.Alloc((nuint)2 + delegateCount, (nuint)IntPtr.Size);
var vtable = obj + 2;
*obj = (IntPtr)vtable;
var handle = GCHandle.Alloc(implementation);
*(obj + 1) = GCHandle.ToIntPtr(handle);
{functionPointers}
Object = (IntPtr)obj;
}
public IntPtr Object { get; private set; }
public static {typeName} Wrap({interfaceName} implementation) => new(implementation);
public static implicit operator IntPtr({typeName} stub) => stub.Object;
~{typeName}()
{
Dispose();
}
public void Dispose()
{
if (Object != IntPtr.Zero)
{
NativeMemory.Free((void*)Object);
Object = IntPtr.Zero;
}
GC.SuppressFinalize(this);
}
private static class Exports
{
{exports}
}
}
}
""");
如果你对某些部分不理解,请记得查看前一篇文章。这里唯一的新内容是析构函数和 Dispose 方法,我们在其中调用 NativeMemory.Free 来释放为该对象分配的内存。接下来,我们需要填充所有的模板部分: {visibility} 、 {typeName} 、 {interfaceName} 、 {delegateCount} 、 {functionPointers} 和 {exports} .
首先是简单的部分:
var interfaceName = symbol.ToString();
var typeName = $"{symbol.Name}";
var visibility = symbol.DeclaredAccessibility.ToString().ToLower();
// To be filled later
int delegateCount = 0;
var exports = new StringBuilder();
var functionPointers = new StringBuilder();
对于一个接口 MyProfiler.ICorProfilerCallback ,我们将生成一个类型为 NativeObjects.ICorProfilerCallback 的包装器。这就是为什么我们将完全限定名存储在 interfaceName (= MyProfiler.ICorProfilerCallback )中,而仅将类型名存储在 typeName (= ICorProfilerCallback )中.
接下来我们想要生成导出列表及其函数指针。我希望源代码生成器支持继承,以避免代码重复,因为 ICorProfilerCallback13 实现了 ICorProfilerCallback12 ,而 ICorProfilerCallback12 本身又实现了 ICorProfilerCallback11 ,依此类推。因此我们提取目标接口继承自的接口列表,并为它们中的每一个提取方法:
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: Inspect the method
}
}
对于一个 QueryInterface(in Guid guid, out IntPtr ptr) 方法,我们将生成的导出看起来像这样:
[UnmanagedCallersOnly]
public static int QueryInterface(IntPtr* self, Guid* __arg1, IntPtr* __arg2)
{
var handleAddress = *(self + 1);
var handle = GCHandle.FromIntPtr(handleAddress);
var obj = (IUnknown)handle.Target;
var result = obj.QueryInterface(*__arg1, out var __local2);
*__arg2 = __local2;
return result;
}
由于这些方法是实例方法,我们添加了 IntPtr* self 参数。另外,如果托管接口中的函数带有 in/out/ref 关键字修饰,我们将参数声明为指针类型,因为 UnmanagedCallersOnly 方法不支持 in/out/ref .
生成导出所需的代码为:
var parameterList = new StringBuilder();
parameterList.Append("IntPtr* self");
foreach (var parameter in method.Parameters)
{
var isPointer = parameter.RefKind == RefKind.None ? "" : "*";
parameterList.Append($", {parameter.Type}{isPointer} __arg{parameter.Ordinal}");
}
exports.AppendLine($" [UnmanagedCallersOnly]");
exports.AppendLine($" public static {method.ReturnType} {method.Name}({parameterList})");
exports.AppendLine($" {{");
exports.AppendLine($" var handle = GCHandle.FromIntPtr(*(self + 1));");
exports.AppendLine($" var obj = ({interfaceName})handle.Target;");
exports.Append($" ");
if (!method.ReturnsVoid)
{
exports.Append("var result = ");
}
exports.Append($"obj.{method.Name}(");
for (int i = 0; i < method.Parameters.Length; i++)
{
if (i > 0)
{
exports.Append(", ");
}
if (method.Parameters[i].RefKind == RefKind.In)
{
exports.Append($"*__arg{i}");
}
else if (method.Parameters[i].RefKind is RefKind.Out)
{
exports.Append($"out var __local{i}");
}
else
{
exports.Append($"__arg{i}");
}
}
exports.AppendLine(");");
for (int i = 0; i < method.Parameters.Length; i++)
{
if (method.Parameters[i].RefKind is RefKind.Out)
{
exports.AppendLine($" *__arg{i} = __local{i};");
}
}
if (!method.ReturnsVoid)
{
exports.AppendLine($" return result;");
}
exports.AppendLine($" }}");
exports.AppendLine();
exports.AppendLine();
对于函数指针,给定与前面相同的方法,我们希望建立:
*(vtable + 1) = (IntPtr)(delegate* unmanaged<IntPtr*, Guid*, IntPtr*>)&Exports.QueryInterface;
生成代码如下:
var sourceArgsList = new StringBuilder();
sourceArgsList.Append("IntPtr _");
for (int i = 0; i < method.Parameters.Length; i++)
{
sourceArgsList.Append($", {method.Parameters[i].OriginalDefinition} a{i}");
}
functionPointers.Append($" *(vtable + {delegateCount}) = (IntPtr)(delegate* unmanaged<IntPtr*");
for (int i = 0; i < method.Parameters.Length; i++)
{
functionPointers.Append($", {method.Parameters[i].Type}");
if (method.Parameters[i].RefKind != RefKind.None)
{
functionPointers.Append("*");
}
}
if (method.ReturnsVoid)
{
functionPointers.Append(", void");
}
else
{
functionPointers.Append($", {method.ReturnType}");
}
functionPointers.AppendLine($">)&Exports.{method.Name};");
delegateCount++;
我们在接口的每个方法都完成了这个操作后,我们只需替换模板中的值并添加生成的源文件:
sourceBuilder.Replace("{typeName}", typeName);
sourceBuilder.Replace("{visibility}", visibility);
sourceBuilder.Replace("{exports}", exports.ToString());
sourceBuilder.Replace("{interfaceName}", interfaceName);
sourceBuilder.Replace("{delegateCount}", delegateCount.ToString());
sourceBuilder.Replace("{functionPointers}", functionPointers.ToString());
context.AddSource($"{symbol.ContainingNamespace?.Name ?? "_"}.{symbol.Name}.g.cs", sourceBuilder.ToString());
就这样,我们的源代码生成器现在准备好了.
要使用我们的源代码生成器,我们可以声明 IUnknown 、 IClassFactory 和 ICorProfilerCallback 接口,并用 [NativeObject] 属性修饰它们:
[NativeObject]
public interface IUnknown
{
HResult QueryInterface(in Guid guid, out IntPtr ptr);
int AddRef();
int Release();
}
[NativeObject]
internal interface IClassFactory : IUnknown
{
HResult CreateInstance(IntPtr outer, in Guid guid, out IntPtr instance);
HResult LockServer(bool @lock);
}
[NativeObject]
public unsafe interface ICorProfilerCallback : IUnknown
{
HResult Initialize(IntPtr pICorProfilerInfoUnk);
// 70+ 多个方法,在这里省略
}
然后我们实现 IClassFactory 并调用 NativeObjects.IClassFactory.Wrap 来创建本机包装器并暴露我们的 ICorProfilerCallback 实例:
public unsafe class ClassFactory : IClassFactory
{
private NativeObjects.IClassFactory _classFactory;
private CorProfilerCallback2 _corProfilerCallback;
public ClassFactory()
{
_classFactory = NativeObjects.IClassFactory.Wrap(this);
}
// The native wrapper has an implicit cast operator to IntPtr
public IntPtr Object => _classFactory;
public HResult CreateInstance(IntPtr outer, in Guid guid, out IntPtr instance)
{
Console.WriteLine("[Profiler] ClassFactory - CreateInstance");
_corProfilerCallback = new();
instance = _corProfilerCallback.Object;
return HResult.S_OK;
}
public HResult LockServer(bool @lock)
{
return default;
}
public HResult QueryInterface(in Guid guid, out IntPtr ptr)
{
Console.WriteLine("[Profiler] ClassFactory - QueryInterface - " + guid);
if (guid == KnownGuids.ClassFactoryGuid)
{
ptr = Object;
return HResult.S_OK;
}
ptr = IntPtr.Zero;
return HResult.E_NOTIMPL;
}
public int AddRef()
{
return 1; // TODO: 做实际的引用计数
}
public int Release()
{
return 0; // TODO: 做实际的引用计数
}
}
并在 DllGetClassObject 中暴露它:
public class DllMain
{
private static ClassFactory Instance;
[UnmanagedCallersOnly(EntryPoint = "DllGetClassObject")]
public static unsafe int DllGetClassObject(void* rclsid, void* riid, nint* ppv)
{
Console.WriteLine("[Profiler] DllGetClassObject");
Instance = new ClassFactory();
*ppv = Instance.Object;
return 0;
}
}
最后,我们可以实现 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 in next article
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;
}
// Stripped for brevity: the default implementation of all 70+ methods of the interface
// Automatically generated by the IDE
}
如果我们使用一个测试应用程序运行它,我们会发现这些功能能按预期工作:
[Profiler] DllGetClassObject
[Profiler] ClassFactory - CreateInstance
[Profiler] ICorProfilerCallback2 - QueryInterface
[Profiler] ICorProfilerCallback2 - Initialize
Hello, World!
在下一步中,我们将处理拼图的最后一个缺失部分:实现ICorProfilerCallback.Initialize方法并获取ICorProfilerInfo的实例。这样我们就拥有了与性能分析器API实际交互所需的一切.
相信大家在开发中经常会遇到一些性能问题,苦于没有有效的工具去发现性能瓶颈,或者是发现瓶颈以后不知道该如何优化。之前一直有读者朋友询问有没有技术交流群,但是由于各种原因一直都没创建,现在很高兴的在这里宣布,我创建了一个专门交流.NET性能优化经验的群组,主题包括但不限于:
如何找到.NET性能瓶颈,如使用APM、dotnet tools等工具 。
.NET框架底层原理的实现,如垃圾回收器、JIT等等 。
如何编写高性能的.NET代码,哪些地方存在性能陷阱 。
希望能有更多志同道合朋友加入,分享一些工作中遇到的.NET性能问题和宝贵的性能分析优化经验。 目前一群已满,现在开放二群.
如果提示已经达到200人,可以加我微信,我拉你进群: lishi-wk 。
另外也创建了 QQ群 ,群号: 687779078,欢迎大家加入.
感谢大家对我公众号的支持与陪伴!为庆祝公众号一周年,抽奖送出一些书籍,请大家关注公众号后续推文! 。
最后此篇关于使用C#编写.NET分析器(三)的文章就讲到这里了,如果你想了解更多关于使用C#编写.NET分析器(三)的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我在网上搜索但没有找到任何合适的文章解释如何使用 javascript 使用 WCF 服务,尤其是 WebScriptEndpoint。 任何人都可以对此给出任何指导吗? 谢谢 最佳答案 这是一篇关于
我正在编写一个将运行 Linux 命令的 C 程序,例如: cat/etc/passwd | grep 列表 |剪切-c 1-5 我没有任何结果 *这里 parent 等待第一个 child (chi
所以我正在尝试处理文件上传,然后将该文件作为二进制文件存储到数据库中。在我存储它之后,我尝试在给定的 URL 上提供文件。我似乎找不到适合这里的方法。我需要使用数据库,因为我使用 Google 应用引
我正在尝试制作一个宏,将下面的公式添加到单元格中,然后将其拖到整个列中并在 H 列中复制相同的公式 我想在 F 和 H 列中输入公式的数据 Range("F1").formula = "=IF(ISE
问题类似于this one ,但我想使用 OperatorPrecedenceParser 解析带有函数应用程序的表达式在 FParsec . 这是我的 AST: type Expression =
我想通过使用 sequelize 和 node.js 将这个查询更改为代码取决于在哪里 select COUNT(gender) as genderCount from customers where
我正在使用GNU bash,版本5.0.3(1)-发行版(x86_64-pc-linux-gnu),我想知道为什么简单的赋值语句会出现语法错误: #/bin/bash var1=/tmp
这里,为什么我的代码在 IE 中不起作用。我的代码适用于所有浏览器。没有问题。但是当我在 IE 上运行我的项目时,它发现错误。 而且我的 jquery 类和 insertadjacentHTMl 也不
我正在尝试更改标签的innerHTML。我无权访问该表单,因此无法编辑 HTML。标签具有的唯一标识符是“for”属性。 这是输入和标签的结构:
我有一个页面,我可以在其中返回用户帖子,可以使用一些 jquery 代码对这些帖子进行即时评论,在发布新评论后,我在帖子下插入新评论以及删除 按钮。问题是 Delete 按钮在新插入的元素上不起作用,
我有一个大约有 20 列的“管道分隔”文件。我只想使用 sha1sum 散列第一列,它是一个数字,如帐号,并按原样返回其余列。 使用 awk 或 sed 执行此操作的最佳方法是什么? Accounti
我需要将以下内容插入到我的表中...我的用户表有五列 id、用户名、密码、名称、条目。 (我还没有提交任何东西到条目中,我稍后会使用 php 来做)但由于某种原因我不断收到这个错误:#1054 - U
所以我试图有一个输入字段,我可以在其中输入任何字符,但然后将输入的值小写,删除任何非字母数字字符,留下“。”而不是空格。 例如,如果我输入: 地球的 70% 是水,-!*#$^^ & 30% 土地 输
我正在尝试做一些我认为非常简单的事情,但出于某种原因我没有得到想要的结果?我是 javascript 的新手,但对 java 有经验,所以我相信我没有使用某种正确的规则。 这是一个获取输入值、检查选择
我想使用 angularjs 从 mysql 数据库加载数据。 这就是应用程序的工作原理;用户登录,他们的用户名存储在 cookie 中。该用户名显示在主页上 我想获取这个值并通过 angularjs
我正在使用 autoLayout,我想在 UITableViewCell 上放置一个 UIlabel,它应该始终位于单元格的右侧和右侧的中心。 这就是我想要实现的目标 所以在这里你可以看到我正在谈论的
我需要与 MySql 等效的 elasticsearch 查询。我的 sql 查询: SELECT DISTINCT t.product_id AS id FROM tbl_sup_price t
我正在实现代码以使用 JSON。 func setup() { if let flickrURL = NSURL(string: "https://api.flickr.com/
我尝试使用for循环声明变量,然后测试cols和rols是否相同。如果是,它将运行递归函数。但是,我在 javascript 中执行 do 时遇到问题。有人可以帮忙吗? 现在,在比较 col.1 和
我举了一个我正在处理的问题的简短示例。 HTML代码: 1 2 3 CSS 代码: .BB a:hover{ color: #000; } .BB > li:after {
我是一名优秀的程序员,十分优秀!