译者注

这是在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-2-8039da001e43

项目链接:https://github.com/kevingosse/ManagedDotnetProfiler

使用C#编写.NET分析器-第一部分:https://mp.weixin.qq.com/s/faa9CFD2sEyGdiLMFJnyxw

正文

在第一部分中,我们看到了如何模仿COM对象的布局,并用它来暴露一个假的IClassFactory实例。它运行得很好,但是我们的解决方案使用了静态方法,所以在需要处理多个实例时跟踪对象状态不太方便。如果我们能将COM对象映射到.NET中的一个实际对象实例,那就太好了。

目前,我们的代码看起来是这样的:

public class DllMain
{
private static ClassFactory Instance; [UnmanagedCallersOnly(EntryPoint = "DllGetClassObject")]
public static unsafe int DllGetClassObject(void* rclsid, void* riid, nint* ppv)
{
Console.WriteLine("Hello from the profiling API"); // 为虚方法表指针和指向5个方法的指针分配内存块
var chunk = (IntPtr*)NativeMemory.Alloc(1 + 5, (nuint)IntPtr.Size); // 虚方法表指针
*chunk = (IntPtr)(chunk + 1); // 指向接口的每个方法的指针
*(chunk + 1) = (IntPtr)(delegate* unmanaged<IntPtr, Guid*, IntPtr*, int>)&QueryInterface;
*(chunk + 2) = (IntPtr)(delegate* unmanaged<IntPtr, int>)&AddRef;
*(chunk + 3) = (IntPtr)(delegate* unmanaged<IntPtr, int>)&Release;
*(chunk + 4) = (IntPtr)(delegate* unmanaged<IntPtr, IntPtr, Guid*, IntPtr*, int>)&CreateInstance;
*(chunk + 5) = (IntPtr)(delegate* unmanaged<IntPtr, bool, int>)&LockServer; *ppv = (IntPtr)chunk; return HResult.S_OK;
} [UnmanagedCallersOnly]
public static unsafe int QueryInterface(IntPtr self, Guid* guid, IntPtr* ptr)
{
Console.WriteLine("QueryInterface");
*ptr = IntPtr.Zero;
return 0;
} [UnmanagedCallersOnly]
public static int AddRef(IntPtr self)
{
Console.WriteLine("AddRef");
return 1;
} [UnmanagedCallersOnly]
public static int Release(IntPtr self)
{
Console.WriteLine("Release");
return 1;
} [UnmanagedCallersOnly]
public static unsafe int CreateInstance(IntPtr self, IntPtr outer, Guid* guid, IntPtr* instance)
{
Console.WriteLine("CreateInstance");
*instance = IntPtr.Zero;
return 0;
} [UnmanagedCallersOnly]
public static int LockServer(IntPtr self, bool @lock)
{
return 0;
}
}

理想情况下,我们希望有一个实际的对象,带有实例方法,如下所示:

public class ClassFactory
{
public unsafe int QueryInterface(IntPtr self, Guid* guid, IntPtr* ptr)
{
Console.WriteLine("QueryInterface");
*ptr = IntPtr.Zero;
return 0;
} public int AddRef(IntPtr self)
{
Console.WriteLine("AddRef");
return 1;
} public int Release(IntPtr self)
{
Console.WriteLine("Release");
return 1;
} public unsafe int CreateInstance(IntPtr self, IntPtr outer, Guid* guid, IntPtr* instance)
{
Console.WriteLine("CreateInstance");
*instance = IntPtr.Zero;
return 0;
} public int LockServer(IntPtr self, bool @lock)
{
return 0;
}
}

然而,原生端只能调用用UnmanagedCallersOnly属性修饰的方法,而这个属性只能应用于静态方法。因此,我们需要一组静态方法,以及从这些静态方法中检索对象实例的方法。

实现这一点的关键是这些方法的self参数。因为我们模仿C++对象的布局,本地对象实例的地址作为第一个参数传递。我们可以使用它来检索我们的托管对象并调用非静态版本的方法。例如:

public unsafe class ClassFactory
{
private static Dictionary<IntPtr, ClassFactory> _instances = new(); public ClassFactory()
{
// 为虚拟表指针和指向5个方法的指针分配内存块
var chunk = (IntPtr*)NativeMemory.Alloc(1 + 5, (nuint)IntPtr.Size); // 指向虚拟表的指针
chunk = (IntPtr)(chunk + 1); // 指向接口中每个方法的指针
(chunk + 1) = (IntPtr)(delegate unmanaged<IntPtr, Guid, IntPtr*, int>)&QueryInterfaceNative; // [...] (为简洁起见,已省略) _instances.Add((IntPtr)chunk, this);
} public int QueryInterface(Guid* guid, IntPtr* ptr)
{
Console.WriteLine("QueryInterface");
ptr = IntPtr.Zero;
return 0;
} // [...] (对于ClassFactory的其他实例方法也是如此) [UnmanagedCallersOnly]
public static int QueryInterfaceNative(IntPtr self, Guid guid, IntPtr* ptr)
{
var instance = _instances[self]; return instance.QueryInterface(guid, ptr);
} // [...] (对于ClassFactory的其他静态方法也是如此)
}

在构造函数中,我们将ClassFactory的实例添加到一个静态字典中,并关联到相应的本地对象的地址。在静态的QueryInterfaceNative方法中,我们从静态字典中检索该实例,并调用非静态的QueryInterface方法。

这是可行的,但每次调用方法时都要进行字典查找是很遗憾的。而且,我们需要处理并发(可能需要使用ConcurrentDictionary)。有没有更好的解决方案?

我们已经有了一个指向本地对象的指针,所以如果本地对象可以存储一个指向托管对象的指针就太好了。像这样:

public ClassFactory()
{
// 为虚拟表指针+托管对象地址+指向5个方法的指针分配内存块
var chunk = (IntPtr*)NativeMemory.Alloc(2 + 5, (nuint)IntPtr.Size); // 指向虚拟表的指针
*chunk = (IntPtr)(chunk + 2); // 指向托管对象的指针
*(chunk + 1) = &this; // [...]
}

如果我们有了这个,那么从静态方法中只需获取指向托管对象的指针就可以了:

[UnmanagedCallersOnly]
public static unsafe int QueryInterfaceNative(IntPtr* self, Guid* guid, IntPtr* ptr)
{
var instance = *(ClassFactory*)(self + 1); return instance.QueryInterface(guid, ptr);
}

但是&this不能编译*,原因很充分:托管对象可能会在任何时候被垃圾回收器移动,所以指针在下一次垃圾回收时可能变得无效。

*: 我撒谎了。如果你使用的是最新版本的C#,那么你可以获取this的地址:

var classFactory = this;

(chunk + 1) = (nint)(nint)&classFactory;

但是由于上述原因,这是不安全的,所以除非你知道自己在做什么,否则请不要这样做。

你可能会想要将对象固定来解决这个问题,但是你不能将一个有对其他托管对象引用的对象固定,所以这也不好。

我们需要的是一种指向托管对象的固定引用,幸运的是,GCHandle正好提供了这样的功能。如果我们为一个托管对象分配一个GCHandle,我们可以使用GCHandle.ToIntPtr获取与该句柄关联的固定地址,并使用GCHandle.FromIntPtr从该地址检索句柄。因此,我们可以这样做:

public ClassFactory()
{
// 为虚拟表指针、托管对象地址以及5个方法的指针分配内存块
var chunk = (IntPtr*)NativeMemory.Alloc(2 + 5, (nuint)IntPtr.Size); // 虚拟表指针
*chunk = (IntPtr)(chunk + 2); // 托管对象指针
var handle = GCHandle.Alloc(this);
*(chunk + 1) = GCHandle.ToIntPtr(handle); // [...]
}

接着,我们可以从静态方法中检索句柄和关联对象:

[UnmanagedCallersOnly]
public static unsafe int QueryInterfaceNative(IntPtr\* self, Guid* guid, IntPtr* ptr)
{
var handleAddress = *(self + 1);
var handle = GCHandle.FromIntPtr(handleAddress);
var instance = (ClassFactory)handle.Target; return instance.QueryInterface(guid, ptr);
}

将所有内容整合在一起,我们的ClassFactory现在看起来像这样:

public unsafe class ClassFactory
{
public ClassFactory()
{
// Allocate the chunk of memory for the vtable pointer + the address of the managed object + the pointers to the 5 methods
var chunk = (IntPtr*)NativeMemory.Alloc(2 + 5, (nuint)IntPtr.Size); // Pointer to the vtable
*chunk = (IntPtr)(chunk + 2); // Pointer to the managed object
var handle = GCHandle.Alloc(this);
*(chunk + 1) = GCHandle.ToIntPtr(handle); *(chunk + 2) = (IntPtr)(delegate* unmanaged<IntPtr*, Guid*, IntPtr*, int>)&Exports.QueryInterface;
*(chunk + 3) = (IntPtr)(delegate* unmanaged<IntPtr*, int>)&Exports.AddRef;
*(chunk + 4) = (IntPtr)(delegate* unmanaged<IntPtr*, int>)&Exports.Release;
*(chunk + 5) = (IntPtr)(delegate* unmanaged<IntPtr*, IntPtr, Guid*, IntPtr*, int>)&Exports.CreateInstance;
*(chunk + 6) = (IntPtr)(delegate* unmanaged<IntPtr*, bool, int>)&Exports.LockServer; Object = (IntPtr)chunk;
} public IntPtr Object { get; } public int QueryInterface(Guid* guid, IntPtr* ptr)
{
Console.WriteLine("QueryInterface");
*ptr = IntPtr.Zero;
return 0;
} public int AddRef()
{
Console.WriteLine("AddRef");
return 1;
} public int Release()
{
Console.WriteLine("Release");
return 1;
} public int CreateInstance(IntPtr outer, Guid* guid, IntPtr* instance)
{
Console.WriteLine("CreateInstance");
*instance = IntPtr.Zero;
return 0;
} public int LockServer(bool @lock)
{
Console.WriteLine("LockServer");
return 0;
} private class Exports
{
[UnmanagedCallersOnly]
public static int QueryInterface(IntPtr* self, Guid* guid, IntPtr* ptr)
{
var handleAddress = *(self + 1);
var handle = GCHandle.FromIntPtr(handleAddress);
var obj = (ClassFactory)handle.Target; return obj.QueryInterface(guid, ptr);
} [UnmanagedCallersOnly]
public static int AddRef(IntPtr* self)
{
var handleAddress = *(self + 1);
var handle = GCHandle.FromIntPtr(handleAddress);
var obj = (ClassFactory)handle.Target; return obj.AddRef();
} [UnmanagedCallersOnly]
public static int Release(IntPtr* self)
{
var handleAddress = *(self + 1);
var handle = GCHandle.FromIntPtr(handleAddress);
var obj = (ClassFactory)handle.Target; return obj.Release();
} [UnmanagedCallersOnly]
public static unsafe int CreateInstance(IntPtr* self, IntPtr outer, Guid* guid, IntPtr* instance)
{
var handleAddress = *(self + 1);
var handle = GCHandle.FromIntPtr(handleAddress);
var obj = (ClassFactory)handle.Target; return obj.CreateInstance(outer, guid, instance);
} [UnmanagedCallersOnly]
public static int LockServer(IntPtr* self, bool @lock)
{
var handleAddress = *(self + 1);
var handle = GCHandle.FromIntPtr(handleAddress);
var obj = (ClassFactory)handle.Target; return obj.LockServer(@lock);
}
}
}

(注意,我将静态方法移到了一个嵌套类中,以避免名称冲突)

我们可以从入口点使用它:

public class DllMain
{
private static ClassFactory Instance; [UnmanagedCallersOnly(EntryPoint = "DllGetClassObject")]
public static unsafe int DllGetClassObject(void* rclsid, void* riid, nint* ppv)
{
Instance = new ClassFactory(); Console.WriteLine("来自分析API的问候"); *ppv = Instance.Object; return HResult.S_OK;
}
}

剩下的就是为ICorProfilerCallback及其约70个方法做这个。我们不打算手动完成这个任务,所以下一篇文章中我们将编写一个源代码生成器来自动化这个过程。

使用C#编写.NET分析器-第二部分的更多相关文章

  1. python之编写购物车(第二天)

    作业: 编写购物车 具体实现了如下功能: 1.可购买的商品信息显示 2.显示购物车内的商品信息.数量.总金额 3.购物车内的商品数量进行增加.减少和商品的删除 4.用户余额的充值 5.用户购买完成进行 ...

  2. 如何使用VS Code编写Spring Boot (第二弹)

    本篇文章是续<如何使用VS Code编写Spring Boot> 之后,结合自己.net经验捣鼓的小demo,一个简单的CRUD,对于习惯了VS操作模式的.net人员非常方便,强大的智能提 ...

  3. 使用C#编写一个.NET分析器(一)

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

  4. 用 C 语言编写一个简单的垃圾回收器

    人们似乎觉得编写垃圾回收机制是非常难的,是一种仅仅有少数智者和Hans Boehm(et al)才干理解的高深魔法.我觉得编写垃圾回收最难的地方就是内存分配,这和阅读K&R所写的malloc例 ...

  5. atitit.自己动手开发编译器and解释器(2) ------语法分析,语义分析,代码生成--attilax总结

    atitit.自己动手开发编译器and解释器(2) ------语法分析,语义分析,代码生成--attilax总结 1. 建立AST 抽象语法树 Abstract Syntax Tree,AST) 1 ...

  6. 《[MySQL技术内幕:SQL编程》读书笔记

    <[MySQL技术内幕:SQL编程>读书笔记 2019年3月31日23:12:11 严禁转载!!! <MySQL技术内幕:SQL编程>这本书是我比较喜欢的一位国内作者姜承尧, ...

  7. F#周报2019年第28期

    新闻 FableConf门票开始贩售 Bolero的HTML模板支持热加载 Bolero从v0.4到v0.5的升级指南 完整的SAFE-Chat迁移至了Fable 2 为纯函数式3D图形生成领域专用语 ...

  8. OO_Unit4_Summary暨课程总结

    初始oo,有被往届传言给吓到:oo进行中,也的确有时会被作业困扰(debug到差点放弃):而oo即将结束的此刻,却又格外感慨这段oo历程. 一.单元架构设计 本单元任务是设计一个UML解析器,能够支持 ...

  9. 团队开发——冲刺2.f

    冲刺阶段二(第六天) 1.昨天做了什么? 编写软件测试计划书第二部分:游戏中新增3个道具(变大.变小.延时). 2.今天准备做什么? 1) 编写软件计划书第三阶段(项目任务.实施计划.风险管理): 2 ...

  10. JS之模板技术(aui / artTemplate)

    artTemplate是个好东西啊,一个开源的js前端模板引擎,使用简单,渲染效率特别的高. 我经常使用这个技术来在前端动态生成新闻列表,排行榜,历史记录等需要在前端列表显示的信息. 下面是artTe ...

随机推荐

  1. IBM Cloud Computing Practitioners 2019 (IBM云计算从业者2019)Exam答案

    Cloud Computing Practitioners 2019 IBM Cloud Computing Practitioners 2019 (IBM云计算从业者2019)Exam答案,加粗的为 ...

  2. jinjia2基本用法

    前言这几年一直在it行业里摸爬滚打,一路走来,不少总结了一些python行业里的高频面试,看到大部分初入行的新鲜血液,还在为各样的面试题答案或收录有各种困难问题 于是乎,我自己开发了一款面试宝典,希望 ...

  3. pysimplegui之系统托盘图标创建

    在 PySimpleGUI(tkinter 版本)上运行时,系统托盘图标为 PNG 和 GIF 格式.PNG.GIF 和 ICO 格式适用于 Wx 和 Qt 端口. 指定"图标"时 ...

  4. pyhon之编译成exe

    1安装pyinstaller pip install pyinstaller 2 编译 pyinstaller -F -w game.py  (-F表示打包单个文件,-w是为了打开exe时候不弹出黑框 ...

  5. 【深度学习】【图像分类网络】(一)残差神经网络ResNet以及组卷积ResNeXt

    ResNet网络 论文:Deep Residual Learning for Image Recognition 网络中的亮点: 1 超深的网络结构(突破了1000层) 上图为简单堆叠卷积层和池化层的 ...

  6. 在.NET 6.0中自定义接口路由

    大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的垫脚石,让我们一起精进. 在本文中,我们将讨论ASP.NET Core中的新路由.我们将了解什么是接口(en ...

  7. MySql中执行计划如何来的——Optimizer Trace

    作者:京东物流 籍磊 1.前言 当谈到MySQL的执行计划时,会有很多同学想:"我就觉得使用其他的执行方案比EXPLAIN语句输出的方案强,凭什么优化器做的决定与我得不一样?".这 ...

  8. 【深入浅出 Yarn 架构与实现】6-3 NodeManager 分布式缓存

    不要跳过这部分知识,对了解 NodeManager 本地目录结构,和熟悉 Container 启动流程有帮助. 一.分布式缓存介绍 主要作用就是将用户应用程序执行时,所需的外部文件资源下载缓存到各个节 ...

  9. 2022-01-31:迷宫 III。 由空地和墙组成的迷宫中有一个球。球可以向上(u)下(d)左(l)右(r)四个方向滚动,但在遇到墙壁前不会停止滚动。当球停下时,可以选择下一个方向。迷宫中还有一个洞

    2022-01-31:迷宫 III. 由空地和墙组成的迷宫中有一个球.球可以向上(u)下(d)左(l)右(r)四个方向滚动,但在遇到墙壁前不会停止滚动.当球停下时,可以选择下一个方向.迷宫中还有一个洞 ...

  10. 2021-06-28:最接近目标值的子序列和。给你一个整数数组 nums 和一个目标值 goal 。你需要从 nums 中选出一个子序列,使子序列元素总和最接近 goal 。也就是说,如果子序列元素和

    2021-06-28:最接近目标值的子序列和.给你一个整数数组 nums 和一个目标值 goal .你需要从 nums 中选出一个子序列,使子序列元素总和最接近 goal .也就是说,如果子序列元素和 ...