
一:背景
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公司解决场景问题提供了一个思考点。