使用C#编写.NET分析器-第二部分

部分,NET,分析器,C# · 浏览次数 : 689

小编点评

**代码分析** **类工厂** * `ClassFactory`:负责创建类实例。 * ``QueryInterface``:用于查询类实例的方法。 * ``AddRef``:用于增加类实例的引用。 * ``Release``:用于释放类实例的引用。 **接口** * ``CreateInstance``:用于创建类实例。 **方法** * ``QueryInterface``:用于查询类实例的方法。 * ``AddRef``:用于增加类实例的引用。 * ``Release``:用于释放类实例的引用。 * ``CreateInstance``:用于创建类实例。 **主要** * ``DllGetClassObject``:用于获取类实例的地址。 * ```QueryInterface``:用于查询类实例的方法。 * ``AddRef``:用于增加类实例的引用。 * ``Release``:用于释放类实例的引用。 **主要问题** * ``DllGetClassObject``:用于获取类实例的地址。 * ```QueryInterface``:用于查询类实例的方法。 * ``AddRef``:用于增加类实例的引用。 * ``Release``:用于释放类实例的引用。 **主要解决方案** * 在`DllGetClassObject``中,使用``GetClassObject``方法从``rclsid``中获取类实例的地址。 * 在``QueryInterface``中,使用``GetMethod`方法从类实例中获取方法的名称。 * 在``AddRef`和``Release``中,使用``GetMethod`方法从类实例中获取方法的名称。

正文

译者注

这是在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分析器-第二部分相似的内容:

使用C#编写.NET分析器-第二部分

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

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

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

使用C#编写.NET分析器(完结)

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

4款.NET开源的Redis客户端驱动库

前言 今天给大家推荐4款.NET开源免费的Redis客户端驱动库(以下排名不分先后)。 Redis是什么? Redis全称是REmote DIctionary Service,即远程字典服务。Redis 是一个使用C语言编写的、开源的(遵守 BSD 协议)、支持网络、可基于内存亦可持久化的日志型、K

C# 设置PDF表单不可编辑、或提取PDF表单数据

PDF表单是PDF中的可编辑区域,允许用户填写指定信息。当表单填写完成后,有时候我们可能需要将其设置为不可编辑,以保护表单内容的完整性和可靠性。或者需要从PDF表单中提取数据以便后续处理或分析。 之前文章详细介绍过如何使用免费Spire.PDF库通过C# 创建、填写表单,本文将继续介绍该免费.NET

C#/.NET这些实用的技巧和知识点你都知道吗?

前言 今天大姚给大家分享一些C#/.NET中的实用的技巧和知识点,它们可以帮助我们提升代码质量和编程效率,希望可以帮助到有需要的同学。 .NET使用CsvHelper快速读取和写入CSV文件 本文主要讲解.NET中如何使用CsvHelper这个开源库快速实现CSV文件读取和写入。 https://m

Visual Studio Code安装C#开发工具包并编写ASP.NET Core Web应用

前言 前段时间微软发布了适用于VS Code的C#开发工具包(注意目前该包还属于预发布状态但是可以正常使用),因为之前看过网上的一些使用VS Code搭建.NET Core环境的教程看着还挺复杂的就一直没有尝试使用VS Code来编写.NET Core。不过听说C# 开发工具包提供了一系列功能和扩展

【长文】带你搞明白Redis

Redis,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。 与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每...

看我是如何用C#编写一个小于8KB的贪吃蛇游戏的

译者注:这是Michal Strehovský大佬的一篇文章,他目前在微软.NET Runtime团队工作,主要是负责.NET NativeAOT功能的开发。我在前几天看到这篇文章,非常喜欢,虽然它的内容稍微有点过时(还是使用的.NET Core 3.0),不过其中的一些编程技巧和思维方式很受用,特

c# 如何将程序加密隐藏?

下面将介绍如何通过`LiteDB`将自己的程序进行加密,首先介绍一下`LiteDB`。 ## LiteDB LiteDB是一个轻量级的嵌入式数据库,它是用C#编写的,适用于.NET平台。它的设计目标是提供一个简单易用的数据库解决方案,可以在各种应用程序中使用。 LiteDB使用单个文件作为数据库存储