使用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.pswp.cn/news/282731.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/282731.shtml
英文地址,請注明出處:http://en.pswp.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…