译者注
这是在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://mp.weixin.qq.com/s/faa9CFD2sEyGdiLMFJnyxw
正文
在第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(GeneratorExecutionContextcontext,INamedTypeSymbolsymbol)
方法中。
对于包装器的名称,我们只需使用符号的名称并追加一个后缀:
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上找到。