一:背景
1. 講故事
這篇文章起源于和一家 .NET公司
開線上會議時,提出的一個場景問題,程序出現了非托管內存暴漲,這些非托管內存關聯的對象都囤積在 終結器隊列
中,很顯然這是代碼中沒用 using 及時釋放引發的,而這塊代碼又是第三方組件,你想加也加不了,所以提出了一個設想:能不能設法干預 終結器隊列的 freachable 節段,讓里面的對象提前釋放,而不是等待不穩定的終結器線程來兜底。。。
這個問題我最近也在考慮,畢竟我寫過如何用 harmony 攔截 .net sdk ,用 minhook 攔截 win32api,唯獨這一塊沒有跟大家聊,雖然 eventpipe 給 coreclr 開了很多的日志口子,但怎么說呢? eventpipe 是一種君子之法,和黑客性質的minhook無法相提并論,所以這一篇就詳細的和大家聊一聊。
二:如何攔截 coreclr
1. 一個小案例
為了方便演示,就以攔截 GC.Collect()
方法為例吧,參考代碼如下:
static void Main(){for (int i = 0; i < 3; i++){Console.WriteLine($"Triggering GC #{i}...");GC.Collect();Thread.Sleep(1000);}}
熟悉GC的朋友應該知道 GC.Collect()
下游方法是coreclr!WKS::GCHeap::GarbageCollect()
,我想在命中這個方法的時候執行一點我的自定義邏輯,這里有一點注意的是,鉤子的回調不要回調到 C#,最好采用 SlideCar 的方式,這里使用靜態鏈接,C代碼參考如下:
#include <windows.h>
#include <stdio.h>
#include <MinHook.h>// 1. 使用 extern "C" 防止名稱修飾
#ifdef __cplusplus
extern "C" {
#endif// 2. 定義原始函數類型typedef int(__fastcall* Real_GarbageCollect)(void* pThis, int generation, bool lowMemory, int mode);// 3. 導出函數聲明__declspec(dllexport) BOOL WINAPI InstallGCHook();__declspec(dllexport) void WINAPI UninstallGCHook();#ifdef __cplusplus
}
#endif// 4. 全局變量
static Real_GarbageCollect fpOriginalGarbageCollect = NULL;
static void* pTargetFunction = NULL;// 5. 獲取 coreclr.dll 中的函數地址(關鍵修改點)
static void* GetGCDunctionAddress() {HMODULE hCoreCLR = GetModuleHandleW(L"coreclr.dll");if (!hCoreCLR) {printf("[ERROR] coreclr.dll not loaded\n");return NULL;}// 計算目標地址return (BYTE*)hCoreCLR + 0x30E670; // 替換為你的實際偏移量
}// 6. Detour 函數(保持不變)
int __fastcall Hook_GarbageCollect(void* pThis, int generation, bool lowMemory, int mode) {printf("[GC Hook] this=0x%p, gen=%d, lowMem=%d, mode=%d\n",pThis, generation, lowMemory, mode);if (fpOriginalGarbageCollect) {MH_DisableHook(pTargetFunction);int result = fpOriginalGarbageCollect(pThis, generation, lowMemory, mode);MH_EnableHook(pTargetFunction);return result;}return 0;
}// 7. 安裝Hook(改為自動計算地址)
__declspec(dllexport) BOOL WINAPI InstallGCHook() {pTargetFunction = GetGCDunctionAddress();if (!pTargetFunction) return FALSE;if (MH_Initialize() != MH_OK) {printf("[ERROR] MinHook init failed\n");return FALSE;}MH_STATUS status = MH_CreateHook(pTargetFunction,&Hook_GarbageCollect,(void**)&fpOriginalGarbageCollect);if (status != MH_OK) {printf("[ERROR] CreateHook failed (status=0x%X)\n", status);MH_Uninitialize();return FALSE;}if (MH_EnableHook(pTargetFunction) != MH_OK) {printf("[ERROR] EnableHook failed\n");MH_Uninitialize();return FALSE;}printf("[SUCCESS] Hook installed at 0x%p\n", pTargetFunction);return TRUE;
}// 8. 卸載Hook(保持不變)
__declspec(dllexport) void WINAPI UninstallGCHook() {if (pTargetFunction) {MH_DisableHook(pTargetFunction);MH_RemoveHook(pTargetFunction);}MH_Uninitialize();printf("[INFO] Hook uninstalled\n");
}
然后指定 頭文件,鏈接文件,截圖如下:
上面的 Hook_GarbageCollect
函數就是回調的地方,我用 printf 輸出當前 GarbageCollect 參數信息, 接下來就是 C# 側了,把生成好的 ConsoleApplication2.dll 丟到 C# 的 bin 目錄下,參考代碼如下:
using System;
using System.Runtime.InteropServices;class Program
{[DllImport("ConsoleApplication2.dll", CallingConvention = CallingConvention.StdCall)]public static extern bool InstallGCHook();[DllImport("ConsoleApplication2.dll", CallingConvention = CallingConvention.StdCall)]public static extern void UninstallGCHook();static void Main(){try{if (InstallGCHook()){Console.WriteLine("Hook installed. Press any key to exit...");for (int i = 0; i < 3; i++){Console.WriteLine($"Triggering GC #{i}...");GC.Collect();Thread.Sleep(1000);}}}finally{UninstallGCHook();}}
}
最后運行程序,可以清楚的看到每次 GC.Collect()
都被成功攔截,截圖如下:
如果你很想知道匯編層到底發生了什么變化,可以用 windbg 觀察便知,截圖如下,真的太完美了,經典的 jmp 跳轉。
2. 相對偏移 0x30E670 的疑問
相信有不少人閱讀代碼之后,會對 return (BYTE*)hCoreCLR + 0x30E670;
中的 0x30E670 感興趣,其實這條語句表示函數coreclr!WKS::GCHeap::GarbageCollect
的入口地址,其中的 0x30E670 偏移是怎么知道的呢? 我是用 windbg 觀測的,計算如下:
0:000> lmvm coreclr
Browse full module list
start end module name
00007ff8`508c0000 00007ff8`50d9d000 coreclr (private pdb symbols)
...0:000> x coreclr!WKS::GCHeap::GarbageCollect
00007ff8`50bce670 coreclr!WKS::GCHeap::GarbageCollect (int, bool, int)0:000> ? 00007ff8`50bce670 - 00007ff8`508c0000
Evaluate expression: 3204720 = 00000000`0030e670
卦中的 000000000030e670
便是,相信此時又會有人提一個疑問,不同版本不同環境下的 coreclr 都可以用這個 0x30e670 嗎?很顯然這是不對的, 0x30e670 本質上是相對 模塊
的偏移地址,同版本的coreclr是沒有問題的,不同版本因為代碼結構不一樣,自然相對地址
就不一樣,所以大家需要根據生產環境的coreclr版本提前計算一下偏移值即可。
三:總結
借助 harmony,minhook 兩大工具可以黑進三大代碼領域 .netsdk,win32,coreclr
,這在.NET高級調試體系
下是一枚核武的存在,相信這篇文章也給這家 .NET公司
解決場景問題提供了一個思考點。