使用C#編寫.NET分析器-第二部分

2023-06-30 12:00:42

譯者注

這是在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個方法做這個。我們不打算手動完成這個任務,所以下一篇文章中我們將編寫一個原始碼生成器來自動化這個過程。