譯者注
這是在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
使用C#編寫.NET分析器-三:https://www.cnblogs.com/InCerry/p/writing-a-net-profiler-in-c-sharp-part-3.html
正文
在第1部分,我們瞭解瞭如何使用NativeAOT
讓我們用C#編寫性能分析器,以及如何暴露一個虛假的COM
對象來使用性能分析API。在第2部分,我們完善了方案以使用實例方法而不是靜態方法。在第3部分,我們使用源生成器自動化了流程。目前,我們具有暴露ICorProfilerCallback
實例所需的一切。然而,爲了編寫性能分析器,我們還需要能夠調用ICorProfilerInfo
的方法,這將是本部分的主題。
提醒一下,我們最後得到了以下實現的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
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;
}
// 爲了簡潔起見,這裏省略了接口中所有70多個方法的默認實現。
}
當調用Initialize
時,我們會收到一個IUnknown
的實例。我們需要在其上調用QueryInterface
以檢索到ICorProfilerInfo
的實例。
要將對象暴露給本機代碼,我們已經看到如何創建一個虛假的vtable
。要使用本地對象,正好相反:我們需要讀取它們的vtable
以獲得方法的地址,然後調用它們。
讓我們編寫一個包裝器,用於從IUnknown
的實例中調用方法。因爲虛擬對象將其vtable
的地址存儲爲第一個字段,我們只需要讀取對象位置處的一個指針即可獲得該vtable
。我們將這個邏輯提取到我們的包裝器的一個屬性中,以方便使用:
public unsafe struct Unknown
{
private readonly IntPtr _self;
public Unknown(IntPtr self)
{
_self = self;
}
private IntPtr* VTable => (IntPtr*)*(IntPtr*)_self;
// TODO: 實現 QueryInterface/AddRef/Release
}
注意,我們將該包裝器聲明爲結構(struct
),因爲它不需要任何狀態。最後,這只是一個帶有一些嵌入式邏輯的精美指針。
要調用這些方法,我們從vtable
的相應槽中檢索它們的地址,然後將它們轉換爲函數指針。然後我們只需要調用它們,確保將對象的地址作爲第一個參數傳遞,因爲它們是實例方法:
public HResult QueryInterface(in Guid guid, out IntPtr ptr)
{
var func = (delegate* unmanaged<IntPtr, in Guid, out IntPtr, HResult>)(*VTable);
return func(_self, in guid, out ptr);
}
public int AddRef()
{
var func = (delegate* unmanaged<IntPtr, int>)(*(VTable + 1));
return func(_self);
}
public int Release()
{
var func = (delegate* unmanaged<IntPtr, int>)(*(VTable + 2));
return func(_self);
}
我們的包裝器可以直接在ICorProfilerCallback.Initialize
中使用,以檢索ICorProfilerInfo
的實例:
public HResult Initialize(IntPtr pICorProfilerInfoUnk)
{
Console.WriteLine("[Profiler] ICorProfilerCallback2 - Initialize");
var iCorProfilerInfo3Guid = Guid.Parse("B555ED4F-452A-4E54-8B39-B5360BAD32A0");
var unknown = new Unknown(pICorProfilerInfoUnk);
var result = unknown.QueryInterface(iCorProfilerInfo3Guid, out var ptr);
if (result == HResult.S_OK)
{
Console.WriteLine($"[Profiler] Successfully retrieved an instance of ICorProfilerInfo3: {ptr:x2}");
}
else
{
Console.WriteLine($"[Profiler] Failed with error code: {result:x2}");
}
return HResult.S_OK;
}
要實際使用我們的ICorProfilerInfo
實例,我們需要編寫相同類型的包裝器。但是,由於該接口聲明瞭數十個方法,我們不會手動操作,而是將擴展我們在第3部分編寫的源代碼生成器。
我們的源代碼生成器將填充以下模板:
public unsafe struct {invokerName}
{
private readonly IntPtr _self;
public {invokerName}(IntPtr self)
{
_self = self;
}
private IntPtr* VTable => (IntPtr*)*(IntPtr*)_self;
{invokerFunctions}
}
我們將所有這些內容實現在上一篇文章中描述的EmitStubForInterface(GeneratorExecutionContext context, INamedTypeSymbol symbol)
方法中。
對於包裝器的名稱,我們只需使用符號的名稱並追加一個後綴:
var invokerName = $"{symbol.Name}Invoker";
然後,我們需要填充函數列表。我們聲明一個StringBuilder並開始遍歷目標接口及其父接口的所有函數:
var invokerFunctions = new StringBuilder();
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
}
}
對於每個方法,我們首先編寫簽名:
invokerFunctions.Append($"public {method.ReturnType} {method.Name}(");
for (int i = 0; i < method.Parameters.Length; i++)
{
if (i > 0)
{
invokerFunctions.Append(", ");
}
var refKind = method.Parameters[i].RefKind;
switch (refKind)
{
case RefKind.In:
invokerFunctions.Append("in ");
break;
case RefKind.Out:
invokerFunctions.Append("out ");
break;
case RefKind.Ref:
invokerFunctions.Append("ref ");
break;
}
invokerFunctions.Append($"{method.Parameters[i].Type} a{i}");
}
invokerFunctions.AppendLine(")");
請注意,所有參數均被重命名爲a1、a2、a3...
,以避免在原始方法的參數具有奇怪名稱時可能發生的衝突。
現在我們可以生成方法的主體,從vtable
中獲取方法的地址,並用預期參數調用它:
invokerFunctions.AppendLine("{");
invokerFunctions.Append("var func = (delegate* unmanaged[Stdcall]<IntPtr");
for (int i = 0; i < method.Parameters.Length; i++)
{
invokerFunctions.Append(", ");
var refKind = method.Parameters[i].RefKind;
switch (refKind)
{
case RefKind.In:
invokerFunctions.Append("in ");
break;
case RefKind.Out:
invokerFunctions.Append("out ");
break;
case RefKind.Ref:
invokerFunctions.Append("ref ");
break;
}
invokerFunctions.Append(method.Parameters[i].Type);
}
invokerFunctions.AppendLine($", {method.ReturnType}>)*(VTable + {delegateCount});");
if (method.ReturnType.SpecialType != SpecialType.System_Void)
{
invokerFunctions.Append("return ");
}
invokerFunctions.Append("func(_self");
for (int i = 0; i < method.Parameters.Length; i++)
{
invokerFunctions.Append($", ");
var refKind = method.Parameters[i].RefKind;
switch (refKind)
{
case RefKind.In:
invokerFunctions.Append("in ");
break;
case RefKind.Out:
invokerFunctions.Append("out ");
break;
case RefKind.Ref:
invokerFunctions.Append("ref ");
break;
}
invokerFunctions.Append($"a{i}");
}
invokerFunctions.AppendLine(");");
invokerFunctions.AppendLine("}");
這有很多代碼,但主要是枚舉參數以生成方法調用,以及在方法返回void
時進行特殊處理。
最後但同樣重要的是,我們替換模板中的佔位符:
sourceBuilder.Replace("{invokerFunctions}", invokerFunctions.ToString());
sourceBuilder.Replace("{invokerName}", invokerName);
有了這個,我們可以回到ICorProfilerCallback.Initialize
的實現,並用我們自動生成的實現替換Unknown
:
public HResult Initialize(IntPtr pICorProfilerInfoUnk)
{
Console.WriteLine("[Profiler] ICorProfilerCallback2 - Initialize");
var iCorProfilerInfo3Guid = Guid.Parse("B555ED4F-452A-4E54-8B39-B5360BAD32A0");
var unknown = new NativeObjects.IUnknownInvoker(pICorProfilerInfoUnk);
var result = unknown.QueryInterface(iCorProfilerInfo3Guid, out var ptr);
if (result == HResult.S_OK)
{
Console.WriteLine($"[Profiler] Successfully retrieved an instance of ICorProfilerInfo3: {ptr:x2}");
var corProfilerInfo = new NativeObjects.ICorProfilerInfo3Invoker(ptr);
// Can start interacting with ICorProfilerInfo
}
else
{
Console.WriteLine($"[Profiler] Failed with error code: {result:x2}");
}
return HResult.S_OK;
}
有了這些,我們終於擁有了編寫探查器所需的所有拼圖碎片。
作爲提醒,所有代碼均可在GitHub上找到。
.NET性能優化交流羣
相信大家在開發中經常會遇到一些性能問題,苦於沒有有效的工具去發現性能瓶頸,或者是發現瓶頸以後不知道該如何優化。之前一直有讀者朋友詢問有沒有技術交流羣,但是由於各種原因一直都沒創建,現在很高興的在這裏宣佈,我創建了一個專門交流.NET性能優化經驗的羣組,主題包括但不限於:
-
如何找到.NET性能瓶頸,如使用APM、dotnet tools等工具
-
.NET框架底層原理的實現,如垃圾回收器、JIT等等
-
如何編寫高性能的.NET代碼,哪些地方存在性能陷阱
希望能有更多志同道合朋友加入,分享一些工作中遇到的.NET性能問題和寶貴的性能分析優化經驗。目前一羣已滿,現在開放二羣。
如果提示已經達到200人,可以加我微信,我拉你進羣: lishi-wk
另外也創建了QQ羣,羣號: 687779078,歡迎大家加入。
抽獎送書活動預熱!!!
感謝大家對我公衆號的支持與陪伴!爲慶祝公衆號一週年,抽獎送出一些書籍,請大家關注公衆號後續推文!