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

译者注

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

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

简介

.NET具有非常强大的分析器API(Profiler API,它类似于Java Agent提供的API,但能做的事情比Java Agent多),我们可以通过它密切的监视.NET运行时、在程序运行期间动态的重写方法、在任意时间点遍历线程调用栈等等。但是学习如果使用该API的入门成本非常高。第一个原因是,你必须要你充分了解.NET元数据系统以及工作原理才能实现一些分析器功能。第二个原因是,它所有的文档和示例都是使用C++编写的,而且目前也没有C#的示例。

从理论上来说,大多数语言都可以来编写.NET分析器。例如,这里有人使用Rust的Demo。使用C#几乎是不可能的,如果使用C#和.NET编写一个Profiler,它将与分析的应用程序同事运行,这会导致一些问题:

  • 由于分析器是一个.NET库,因此它最终会分析自身。列如,当JIT编译所分析的应用程序方法时,会引发一些分析的事件,比如JITCompilationStartedJITCompilationStartedJITCompilationStarted等等。这些事件都会调用分析器的回调方法,而由于分析器是.NET库,所以也需要进行编译,又会产生上面的事件,你应该明白我的观点。

  • 即使你设法找到了该问题的修复方法,还有一个更实际的问题:在运行时初始化的过程中,分析器被很早的加载,而这时系统还没有准备好运行.NET代码。

我一直觉得这很可惜,因为C#是所有C#开发人员最熟悉的开发语言。幸运的是,现在情况已经改变了。

我已经在之前的一篇文章中提到过,微软正在积极的研究Native AOT。这个工具允许我们将.NET库编译Native的独立库。独立这是关键:因为它带有自己的运行时(自己的GC、自己的线程池、自己的类型系统....),所以可以将它加载到进程中,看起来和C++、Rust任何Native库一样。这意味我们可以使用Native AOT工具和C#语言来编写一个.NET分析器。

让我们开始

学习如果编写.NET分析器,你可以参考Christophe Nasarre编写的文章。简而言之,我们需要公开一个返回IClassFactory实例的DllGetClassObject方法(熟悉微软COM编程的朋友是不是感觉似曾相识?)。然后.NET Runtime将调用ClassFactory上的CreateInstance方法,该方法将返回一个ICorProfilerCallback实例(或者后面新增的ICorProfilerCallback2,ICorProfilerCallback3,... ,这取决于我们希望支持哪个版本的Profiler API),最后但并非最不重要的是,.NET Runtime将使用一个IUnknown参数调用该实例上的Initialize方法,我们可以使用它来获取我们需要查询Profiler API 的 ICorProfilerInfo (或 ICorProfilerInfo2,ICorProfilerInfo3,...)的实例。

话不多说。让我们从第一步开始: 导出 DllGetClassObject 方法。首先我们创建一个。NET 6类库项目,并添加对Microsoft.DotNet.ILCompiler引用,使用7.0.0-preview.*版本。然后,我们使用 DllGetClassObject 方法创建一个 DllMain 类(名称并不重要)。我们还用一个 UnmanagedCallersOnly属性装饰这个方法,以指示NativeAOT工具链导出该方法。

using System;
using System.Runtime.InteropServices;namespace ManagedDotnetProfiler;public class DllMain
{[UnmanagedCallersOnly(EntryPoint = "DllGetClassObject")]public static unsafe int DllGetClassObject(Guid* rclsid, Guid* riid, IntPtr* ppv){Console.WriteLine("Hello from the profiling API");return 0;}
}

然后我们使用dotnet publish命令,并且带上/p:NativeLib=Shared来发布一个Native库。

dotnet publish /p:NativeLib=Shared /p:SelfContained=true -r win-x64 -c Release

输出是一个.dll文件(在linux上会是一个.so文件)。为了测试一切正常工作,我们可以启动任何.NET控制台应用在设定正确的环境变量后:

set CORECLR_ENABLE_PROFILING=1  # 启用分析器
set CORECLR_PROFILER={B3A10128-F10D-4044-AB27-A799DB8B7E4F} # 分析器 COM Guid
set CORECLR_PROFILER_PATH=C:\git\ManagedDotnetProfiler\ManagedDotnetProfiler\bin\Release\net6.0\win-x64\publish\ManagedDotnetProfiler.dll # 分析器.dll路径

CORECLR_ENABLE_PROFILING指示运行库加载分析器。CORECLR_PROFILER 是唯一标识分析器的 GUID (现在任何值都可以)。CORECLR_PROFILER_ ATH是我们用NativeAOT发布的 dll的路径。如果一切正常,你应该看到在加载目标应用程序期间显示的消息:

C:\console\bin\Debug\net6.0>console.exe  
Hello from the profiling API  
Hello, World!

很好,但是现在还没有什么用。如何编写一个真正的分析器?现在我们需要了解如何公开 IClassFactory 的实例。

公开一个C++接口(类似的行为)

MSDN 文档指出 IClassFactory 是一个接口。但是"接口"在C++和C#中意味着不同的东西,所以我们不能仅仅在我们的.NET代码中定义一个接口,然后收工。

事实上,接口的概念在C++中并不存在。实际上,它只是指定一个只包含纯虚函数的抽象类。因此,我们需要构建和公开一个看起来像C++抽象类的对象。为此,我们需要理解vtable的概念。

假设我们有一个带有单个方法 DoSomething 的接口 IInterface,以及两个实现ClassA和ClassB。因为ClassA和ClassB都可以声明它们自己的DoSomething实现,所以当给定 IInterface实例的指针时,运行时需要间接的知道应该调用哪个实现。这种间接方式称为虚表或 vtable。

按照约定,当类实现虚方法时,C++编译器在对象的开头设置一个隐藏字段。该隐藏字段包含一个指向vtable的指针。vtable是一个内存块,按照声明的顺序包含每个虚方法实现的地址。当调用虚方法时,运行时将首先获取vtable,然后使用它获取实现的地址。

vtable有更多的特性,例如处理多重继承,但是我们不需要了解这些。

总而言之,要创建一个可供C++运行时使用的IClassFactory对象,我们需要分配一块内存来存储函数的地址。这是我们的vtable。然后,我们需要另一块内存,其中包含一个指向 vtable 的指针。如下图所示:e245e2718bdfa2443bf290ea73347a30.png

为了简单的实现它,我们可以将实例和 vtable 合并到一个内存块中:66f452e6353be58b54913ba44dae4746.png

那么它在C#中是什么样子的呢?首先,我们为 IClassFactory 接口中的每个函数声明一个静态方法,并打上UnmanagedCallersOnly的特性:

[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;}

然后,在DllGetClassObject中,我们分配用于存储指向vtable(我们的假实例)和vtable本身的指针的内存块。由于此内存将由本机代码使用,因此必须确保它不会被垃圾收集器移动。我们可以声明一个IntPtr数组并固定它,但是我更喜欢使用NativeMemory。分配GC不会跟踪的内存。要获取静态方法的地址,我们可以将它们转换为函数指针,然后转换为IntPtr。最后,我们通过函数的ppv参数返回内存块的地址。

[UnmanagedCallersOnly(EntryPoint = "DllGetClassObject")]public static unsafe int DllGetClassObject(Guid* rclsid, Guid* riid, IntPtr* ppv){Console.WriteLine("Hello from the profiling API");// 为vtable指针+指向5个方法的指针分配内存块var chunk = (IntPtr*)NativeMemory.Alloc(1 + 5, (nuint)IntPtr.Size);// 指向 vtable*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;}

在编译和测试之后,我们可以看到我们的假 IClassFactory 的 CreateInstance 方法如预期的那样被调用:

C:\console\bin\Debug\net6.0> .\console.exe  
Hello from the profiling API  
CreateInstance  
Release  
Hello, World!

征程才刚刚开始

下一步是实现CreateInstance方法。如前所述,我们希望返回ICorProfilerCallback的实例。为了实现这个接口,我们可以像对 IClassFactory 那样做同样的事情,但是 ICorProfilerCallback包含近70个方法!要编写的样板代码太多了,更不用说 ICorProfilerCallback2、 ICorProfilerCallback3等等了。另外,我们当前的解决方案只能使用静态方法,如果能有一些可以使用实例方法的东西就太好了。在本系列的下一篇文章中,我们将看到如何编写一个源生成器来为我们完成所有枯燥无聊的工作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/282731.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

内置数据类型

Java语言提供了八种基本类型。六种数字类型&#xff08;四个整数型&#xff0c;两个浮点型&#xff09;&#xff0c;一种字符类型&#xff0c;还有一种布尔型。 byte&#xff1a; byte 数据类型是8位、有符号的&#xff0c;以二进制补码表示的整数&#xff1b; 最小值是 -128&…

算法学习之循环结构程序设计

for循环 打印1,2,3&#xff0c;...&#xff0c;n每个占一行。 #include <conio.h> #include<stdio.h> int main(){int i,n;scanf("%d",&n);for(i1;i<n;i){printf("%d\n",i);}getch();return 0; } 分支结合循环&#xff0c;威力很强大 输…

Linux常用命令 (分门别类)

一、系统安全: su: 用于切换当前用户身份到其他用户身份&#xff0c;变更时须输入所要变更的用户帐号与密码 sudo: 用来以其他身份来执行命令&#xff0c;预设的身份为root lastlog: 用于显示系统中所有用户最近一次登录信息 lastb: 用于显示用户错误的登录列表&#x…

hibernate自定义校验器使用(字段在in范围之内)

2019独角兽企业重金招聘Python工程师标准>>> 1.自定义注解类DigitsMustIn Constraint(validatedBy DigitsMustInValidator.class) //具体的实现 Target({java.lang.annotation.ElementType.METHOD,java.lang.annotation.ElementType.FIELD}) Retention(java.lang.a…

sql将html转成excel,使用SQL*PLUS,构建完美excel或html输出

通过SQL*PLUS我们可以构建友好的输出&#xff0c;满足多样化用户需求。本例通过简单示例&#xff0c;介绍通过sql*plus输出xls&#xff0c;html两种格式文件.首先创建两个脚本:1.main.sql用以设置环境&#xff0c;调用具体功能脚本2.功能脚本-get_tables.sql为实现具体功能之脚…

[cogs347]地震

COGS&#xff1a;地震&#xff08;平衡树&#xff09; COGS上一道题。。。文件名是equake 还是又打了一遍板子。。。 加个lazy标记就行了。。。 注意查询时先下传标记&#xff08;lazy&#xff09; // It is made by XZZ #include<cstdio> #include<algorithm> #de…

第八课-第二讲 08_02_bash脚本编程之七 case语句及脚本选项进阶

第八课-第二讲 08_02_bash脚本编程之七 case语句及脚本选项进阶 一. 面向过程控制结构顺序结构选择结构循环结构选择结构if语句 单分支&#xff0c;双分支&#xff0c;多分支case 语句 case语句:选择结构 case SWITCH invalue1)---此处的value是当做字符来比较的statement....…

html表单提交按钮怎么居中,与表单框一致,居中提交按钮_html_开发99编程知识库...

我嘗試將提交按鈕與表單的一個條目對齊失敗。 我只是希望提交按鈕稍微定位到窗體框的右側和中心。 現在是右邊&#xff0c;但在盒子的底部。我試圖回答相似的查詢&#xff0c;對於提交按鈕( 浮點&#xff0c;margin 等等 )&#xff0c;但是我不能找到正確的選擇。我的HTML如下所…

一个简单的WebService服务

现在&#xff0c;网上提供的免费的webservice服务的网站&#xff1a; http://www.webxml.com.cn/从扩展名上看&#xff0c;是 .net构建的网站。看看功能的实现效果&#xff1a;需求&#xff1a;我们要远程调用手机号归属地的查询&#xff1a;开发步骤&#xff1a; 1&#xff0e…

Linux中的vi和vim

一、vi与vim的概念和区别 概念: 它们都是多模式编辑器&#xff0c;不同的是vim 是vi的升级版本&#xff0c;它不仅兼容vi的所有指令&#xff0c;而且还有一些新的特性在里面。 vim优势主要体现在一下几方面: 1、多级撤消 我们知道在vi里&#xff0c;按 u只能撤消上次命令&a…

[工具分享]备份SSAS模型TMSL脚本元数据工具,多给自己一点后悔药可吃。

笔者在2019年分享过自己写的一个小工具&#xff0c;用于备份Sqlserver数据库的元数据。近期在一个PowerBI项目中&#xff0c;发现很有必要也备份下SSAS分析模型的元数据&#xff0c;防止不小心服务器坏了或使用Tabular Editor连接数据库方式开发过程中&#xff0c;不小心覆盖了…

UVA - 11181 数学

UVA - 11181 题意&#xff1a; n个人去买东西&#xff0c;其中第i个人买东西的概率是p[i],最后只有r个人买了东西&#xff0c;求每个人实际买了东西的概率 代码&#xff1a; //在r个人买东西的概率下每个人买了东西的概率&#xff0c;这是条件概率&#xff0c;因为最多20个人可…

js时间戳转成日期格式

//第一种2 function getLocalTime(nS) { 3 return new Date(parseInt(nS) * 1000).toLocaleString().replace(/:\d{1,2}$/, ); 4 } 5 alert(getLocalTime(1293072805));6 //结果是2010年12月23日 10:537 //第二种 8 function getLocalTime(nS) { 9 r…

计算机桌面去方格子,win7桌面office图标变成白色方格图标的原因和解法

win7系统开机发现桌面上所有office图标变成白色方格图标&#xff0c;其他程序图标都正常显示&#xff0c;是怎么回事呢&#xff1f;出现这样的情况&#xff0c;一般是由于文件图标缓存错误或者丢失导致&#xff0c;找打原因后该如何解决问题&#xff1f;可以通过记事本来解决此…

JS获取元素的offsetTop,offsetLeft等相关属性

1. obj.clientWidth //获取元素的宽度 obj.clientHeight //元素的高度 obj.offsetLeft //元素相对于父元素的left obj.offsetTop //元素相对于父元素的top obj.offsetWidth //元素的宽度 obj.offsetHeight //元素的高度 区别&#xff1a; clientWidth width padding clientHe…

vi/vim 三种模式及命令 (简单粗暴,轻松搞懂)

//一般模式(默认模式) 一般模式&#xff1a; 移动光标 h 或 向左方向键 光标向左移动一个字符 j 或 向下方向键 光标向下移动一个字符 k 或 向上方向键 光标向上移动一个字符 l 或 向右方向键 光标向右移动一个字符 [Ctrl] [f] 屏幕『向前』移动一页&#xff08;常用) [Ct…

Kong入门学习实践(1)基础概念快览

【API网关】| 总结/Edison Zhou最近在学习Kong网关&#xff0c;因此根据老习惯&#xff0c;我会将我的学习过程记录下来&#xff0c;一来体系化整理&#xff0c;二来作为笔记供将来翻看。由于我司会直接使用Kong企业版&#xff0c;学习过程中我会使用Kong开源版。什么是Kong&am…

条件锁

ReentrantLock类有一个方法newCondition用来生成这个锁对象的一个条件&#xff08;ConditionObject&#xff09;对象&#xff0c;它实现了Condition接口。Condition提供了线程通讯的一套机制await和signal等线程间进行通讯的方法。。1、适用场景当某线程获取了锁对象&#xff0…

计算机应用技术 平面设计,全国信息化计算机应用技术水平教育考试试卷 平面设计师...

科目编号&#xff1a;4233全国信息化计算机应用技术水平教育考试试卷(考试时间&#xff1a;180分钟 考试总分&#xff1a;100分 专业认证课程&#xff1a;Photoshop 平面设计)注意事项1、 请首先按要求在试卷的标封处填写您的姓名、考号等&#xff1b;2、 请仔细阅读各种题目的…

RabbitMQ之消息模式简单易懂,超详细分享

前言上一篇对RabbitMQ的流程和相关的理论进行初步的概述&#xff0c;如果小伙伴之前对消息队列不是很了解&#xff0c;那么在看理论时会有些困惑&#xff0c;这里以消息模式为切入点&#xff0c;结合理论细节和代码实践的方式一起来学习。正文常用的模式有Simple、Work、Fanout…