翼度科技»论坛 编程开发 .net 查看内容

如何洞察 .NET程序 非托管句柄泄露

6

主题

6

帖子

18

积分

新手上路

Rank: 1

积分
18
一:背景

1. 讲故事

很多朋友可能会有疑问,C# 是一门托管语言,怎么可能会有非托管句柄泄露呢? 其实一旦 C# 程序与 C++ 语言交互之后,往往就会被后者拖入非托管泥潭,让我们这些调试者被迫探究 非托管领域问题。
二:非托管句柄泄露

1. 测试案例

为了方便讲述,我们上一个 Event 泄露的案例,使用 C# 调用 C++ ,然后让 C++ 产生 bug 导致句柄泄露。
先看一下 C++ 代码
  1. extern "C"
  2. {
  3.         _declspec(dllexport) void CSharpCreateEvent();
  4. }
  5. #include "iostream"
  6. #include <Windows.h>
  7. using namespace std;
  8. void CSharpCreateEvent()
  9. {
  10.         HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
  11.         printf("\nEvent句柄值: %#08x        ", hEvent);
  12. }
复制代码
然后导出一个 CSharpCreateEvent 方法给 C# 使用。
  1.     internal class Program
  2.     {
  3.         [DllImport("Example_20_1_5", CallingConvention = CallingConvention.Cdecl)]
  4.         extern static void CSharpCreateEvent();
  5.         static void Main(string[] args)
  6.         {
  7.             try
  8.             {
  9.                 while (true)
  10.                 {
  11.                     Task.Run(() =>
  12.                     {
  13.                         CSharpCreateEvent();
  14.                     });
  15.                     Thread.Sleep(10);
  16.                 }
  17.             }
  18.             catch (Exception ex)
  19.             {
  20.                 Console.WriteLine(ex.Message);
  21.             }
  22.             Console.ReadLine();
  23.         }
  24.     }
复制代码
程序跑起来后,在任务管理器中会发现这个句柄在不断的上涨,截图如下:

2. 到底是谁在泄露

如果你的生产环境可以用 WinDbg 附加进程,那用它就可以轻松解决,可以借助 !handle 命令看一下泄露的句柄类型。
  1. 0:004> !handle
  2. ...
  3. Handle 16fc
  4.   Type                 Event
  5. 1411 Handles
  6. Type                   Count
  7. None                   6
  8. Event                  1337
  9. File                   16
  10. Directory              4
  11. Mutant                 3
  12. WindowStation          2
  13. Semaphore              5
  14. Key                    10
  15. Thread                 8
  16. Desktop                1
  17. IoCompletion           5
  18. TpWorkerFactory        3
  19. ALPC Port              1
  20. WaitCompletionPacket        10
复制代码
从统计信息看,当前 Event 高达 1337 个,看样子程序存在 Event 泄露,接下来我们就要洞察到底是谁分配的 Event,如果能找到分配 Event 的线程栈,那这个问题就会迎刃而解,对吧,有 WinDbg 在,方圆3公里的bug都要移民,追踪调用栈可以使用 WinDbg 提供的 !htrace 命令。
它的原理很简单,一句话表示就是:挖出现在时间点和快照之间那些没有被 free 处理的 handle 调用栈,结果一清二楚,参考代码如下:
  1. 0:011> !htrace -enable
  2. Handle tracing enabled.
  3. Handle tracing information snapshot successfully taken.
  4. 0:011> g
  5. (e14.90c0): Break instruction exception - code 80000003 (first chance)
  6. eax=006f2000 ebx=00000000 ecx=7777dfe0 edx=10088020 esi=7777dfe0 edi=7777dfe0
  7. eip=77744e50 esp=0811f97c ebp=0811f9a8 iopl=0         nv up ei pl zr na pe nc
  8. cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
  9. ntdll!DbgBreakPoint:
  10. 77744e50 cc              int     3
  11. 0:007> !htrace -diff
  12. Handle tracing information snapshot successfully taken.
  13. 0xad new stack traces since the previous snapshot.
  14. Ignoring handles that were already closed...
  15. Outstanding handles opened since the previous snapshot:
  16. --------------------------------------
  17. Handle = 0x0000199c - OPEN
  18. Thread ID = 0x000017c8, Process ID = 0x00000e14
  19. 0x4ac3d761: +0x4ac3d761
  20. 0x4aa0d9f5: +0x4aa0d9f5
  21. 0x6674d9c4: +0x6674d9c4
  22. 0x66547f33: +0x66547f33
  23. 0x6654901a: +0x6654901a
  24. 0x776c17c3: +0x776c17c3
  25. 0x776c11b9: +0x776c11b9
  26. 0x665438c9: +0x665438c9
  27. 0x665432bd: +0x665432bd
  28. 0x66725089: +0x66725089
  29. 0x66724c73: +0x66724c73
  30. 0x66724c1e: +0x66724c1e
  31. 0x77742f7c: ntdll!NtCreateEvent+0x0000000c
  32. 0x770f5746: KERNELBASE!CreateEventExW+0x00000056
  33. 0x770e2b04: KERNELBASE!CreateEventW+0x00000024
  34. *** WARNING: Unable to verify checksum for D:\skyfly\20.20230628\src\Example\Example_20_1_4\bin\x86\Debug\net6.0\Example_20_1_5.DLL
  35. 0x6ac91755: Example_20_1_5!CSharpCreateEvent+0x00000035
  36. --------------------------------------
  37. ...
  38. Displayed 0xaa stack traces for outstanding handles opened since the previous snapshot.
复制代码
从卦中短暂的时间内快照之间有 170 个句柄没有被释放,而且从调用栈看是 Example_20_1_5!CSharpCreateEvent 方法所致,但这里有一个问题,虽然有非托管栈,但没有看到任何托管部分,那怎么办呢?
3. 如何洞察到托管栈

其实这个问题很简单,既然都 WinDbg 附加了,干脆用 bp 下断点,后续泄露之时必然会被命中,然后通过 !clrstack 或者 k 观察线程栈即可,有了思路就开干。
  1. :007> bp Example_20_1_5!CSharpCreateEvent "k; gc"
  2. breakpoint 0 redefined
  3. 0:007> g
  4. # ChildEBP RetAddr      
  5. 00 0848f9e4 080674f3     Example_20_1_5!CSharpCreateEvent [D:\skyfly\20.20230628\src\Example\Example_20_1_5\Example_20_1_5.cpp @ 15]
  6. WARNING: Frame IP not in any known module. Following frames may be wrong.
  7. 01 0848f9e4 0806748b     0x80674f3
  8. 02 0848f9f0 0806e3dd     Example_20_1_4!Example_20_1_4.Program.<>c.<Main>b__1_0+0x1b
  9. 03 0848f9fc 0806e38d     System_Private_CoreLib!System.Threading.Tasks.Task.InnerInvoke+0x3d
  10. 04 0848fa04 0806e307     System_Private_CoreLib!System.Threading.Tasks.Task.<>c.<.cctor>b__272_0+0xd
  11. 05 0848fa2c 0806e072     System_Private_CoreLib!System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop+0x37
  12. 06 0848fa94 0806c49f     System_Private_CoreLib!System.Threading.Tasks.Task.ExecuteWithThreadLocal+0x82
  13. 07 0848faec 6b22f2bc     System_Private_CoreLib!System.Threading.ThreadPoolWorkQueue.Dispatch+0x1bf
  14. 08 0848fb88 6b216595     System_Private_CoreLib!System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart+0xdc [/_/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs @ 63]
  15. 09 0848fb98 6c00c30f     System_Private_CoreLib!System.Threading.Thread.StartCallback+0x35 [/_/src/coreclr/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs @ 106]
  16. 0a 0848fba4 6bf5c07b     coreclr!CallDescrWorkerInternal+0x34
  17. 0b 0848fbd8 6bf6799a     coreclr!CallDescrWorkerWithHandler+0x66 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 69]
  18. 0c 0848fc20 6bff619b     coreclr!DispatchCallSimple+0x7f [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 220]
  19. 0d 0848fc44 6bf7c7df     coreclr!ThreadNative::KickOffThread_Worker+0x4b [D:\a\_work\1\s\src\coreclr\vm\comsynchronizable.cpp @ 158]
  20. 0e (Inline) --------     coreclr!ManagedThreadBase_DispatchInner+0x3d [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 7321]
  21. 0f 0848fcc8 6bf7c70f     coreclr!ManagedThreadBase_DispatchMiddle+0x8c [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 7365]
  22. 10 0848fd20 6bf1116f     coreclr!ManagedThreadBase_DispatchOuter+0x62 [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 7543]
  23. 11 (Inline) --------     coreclr!ManagedThreadBase_FullTransition+0x21 [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 7569]
  24. 12 (Inline) --------     coreclr!ManagedThreadBase::KickOff+0x21 [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 7604]
  25. 13 0848fd54 755b00f9     coreclr!ThreadNative::KickOffThread+0x7f [D:\a\_work\1\s\src\coreclr\vm\comsynchronizable.cpp @ 230]
  26. 14 0848fd64 77737bbe     KERNEL32!BaseThreadInitThunk+0x19
  27. 15 0848fdc0 77737b8e     ntdll!__RtlUserThreadStart+0x2f
  28. 16 0848fdd0 00000000     ntdll!_RtlUserThreadStart+0x1b
  29. ...
复制代码
从卦中看,一切都非常明白,这里再补充一点,如果想中途再产生 快照,可以用 -snapshot 命令创建一个初始点,参考如下:
  1. 0:007> !htrace -snapshot
  2. Handle tracing information snapshot successfully taken.
复制代码
三:总结

handle 泄露也是一个比较难搞的问题,难点在于生产环境可能不让你用 WinDbg 这种侵入方式,但问题还得要解决,必须创造条件上,当前除了 WinDbg 还没有找到其他方式,有机会再研究下吧。

来源:https://www.cnblogs.com/huangxincheng/archive/2023/07/07/17534680.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具