句柄泄露问题追踪

无论是在编写Windows程序还是Linux程序,都可能存在句柄泄露的问题。在Linux中一般来说一个进程的fd使用是有上限的,可以使用ulimit命令进行上限查看,当出现fd泄露的时候,可能会出现socket创建失败,文件打不开等问题。Windows类似,本文主要阐述了对Windows中的句柄泄露的追踪方法。

Windows句柄泄露

在Windows开发中,当调用Windows API,比如CreateFile, CreateEvent, CreateThread 等API的时候,都会返回一个句柄Handle。当相应的资源使用完后,如果没有调用CloseHandle去关闭Handle,则会出现句柄泄露的问题。当这个问题发生的时候,当前进程再调用比如CreateThread会返回Windows Error 1450, 表示Insufficient system resources exist to complete the requested service.,导致程序运行问题。Windows的总句柄数,也是有限制的,此时甚至会影响其他进程的运行。那么接下来让我们来看看如何定位句柄泄露问题吧。

Process Explorer定位句柄泄露

在任务管理器中可以查看一个进程的句柄数量,在Process Explorer中也可以。我们可以这样去定位句柄泄露问题:

  1. 可以在Process Explorer中显示Handles一列,如果进程有句柄泄露问题,那么这个进程的Handles一列的数值会持续的增长
  2. 选中相应的进程,可以观察本进程的句柄详细信息。比如这个句柄,关联的是线程、文件、Event等等。
  3. 当出现句柄泄露的时候,那么会有大量的相似的类型的句柄出现在其中。

如果因为CreateThread的句柄没有释放,导致句柄泄露,那么则可以在句柄详细信息的条目中看到很多Thread类型的。然后查找可能调用CreateThread的代码。
如果因为CreateFile的句柄没有释放,则可以在Process Explorer中查看文件的路径,根据文件的路径来查找可能引起句柄泄露的代码。
在这里插入图片描述
这种方式可以解决一部分句柄泄露问题,但是有时候可能碰到一些场景不能解决:

  1. 一个产品可能依赖于多个第三方模块,当句柄泄露的问题是第三方模块引起的,可能看到泄露句柄类型和名字也难以定位到具体的模块。
  2. Process Explorer不能够显示所有的句柄,比如无名的Event,这样也无法查找。

Windbg定位句柄泄露问题

除了上一章末讲的两个问题,那么有没有一种方法可以定位到这个泄露的句柄申请的地方吗?Windbg就可以做到。
先上一段测试的Sample, 每隔一秒钟创建一个Event,但并没有调用CloseHandle, 会导致Handle Leak。

#include <windows.h>
#include <iostream>
#include <thread>

void HandleLeak()
{ 
	int iCount = 0;
	while(true)
	{
		iCount++;
		HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
		DWORD dwError = GetLastError();
		std::this_thread::sleep_for(std::chrono::seconds(1));
		if (!hEvent || dwError != ERROR_SUCCESS)
		{
			std::cerr << "Number: " << iCount << " Error: "<< dwError << std::endl;
			return;
		}
	}
}

int main()
{
	HandleLeak();
	return 0;
}

第一步 用Windbg attach到你要测试进程
第二步 Windbg中调用命令!htrace -enable: 开启句柄追踪,并且保存当前所有的Handle的快照(Snapshot)

0:006> !htrace -enable
Handle tracing enabled.
Handle tracing information snapshot successfully taken.

第三步 Windobg中调用命令g, 让程序运行一段时间
第四步 菜单Debug->break进入调试,Windbg中运行!htrace -diff: 将进程当前的所有的句柄和之前快照的句柄进行对比,找出这段时间内多出来的句柄。

0:006> !htrace -diff
Handle tracing information snapshot successfully taken.
0x31 new stack traces since the previous snapshot.
Ignoring handles that were already closed...
Outstanding handles opened since the previous snapshot:
--------------------------------------
Handle = 0x0000000000000290 - OPEN
Thread ID = 0x0000000000001ca0, Process ID = 0x0000000000004360

0x00007ffca4dcb2a4: ntdll!NtCreateEvent+0x0000000000000014
0x00007ffca1ebb623: KERNELBASE!CreateEventA+0x0000000000000083
0x00007ff7ea001e94: HandleLeak!HandleLeak+0x0000000000000034
0x00007ff7ea002099: HandleLeak!main+0x0000000000000009
0x00007ff7ea0023d4: HandleLeak!__scrt_common_main_seh+0x000000000000010c
0x00007ffca23f4034: KERNEL32!BaseThreadInitThunk+0x0000000000000014
0x00007ffca4da3691: ntdll!RtlUserThreadStart+0x0000000000000021
--------------------------------------
Handle = 0x0000000000000280 - OPEN
Thread ID = 0x0000000000001ca0, Process ID = 0x0000000000004360

0x00007ffca4dcb2a4: ntdll!NtCreateEvent+0x0000000000000014
0x00007ffca1ebb623: KERNELBASE!CreateEventA+0x0000000000000083
0x00007ff7ea001e94: HandleLeak!HandleLeak+0x0000000000000034
0x00007ff7ea002099: HandleLeak!main+0x0000000000000009
0x00007ff7ea0023d4: HandleLeak!__scrt_common_main_seh+0x000000000000010c
0x00007ffca23f4034: KERNEL32!BaseThreadInitThunk+0x0000000000000014
0x00007ffca4da3691: ntdll!RtlUserThreadStart+0x0000000000000021

第五步 通过上述的Handle调用栈,就很容易能够知道导致句柄泄露的代码了。

以上方法可以比较完美的解决句柄泄露问题,但是如果问题本地难以重现,需要到客户环境中查找句柄泄露问题,那么一般不太建议Symbols拷贝到客户的环境中,以免造成Symbols泄露。那么上述第四步中就无法查看到明确的函数调用栈,可以从客户环境中拷贝出来第四步!htrace -diff的信息,然后再自己本地Load Symbols后通过ln HandleLeak+0x....查看相应的函数调用栈,从而定位问题。

除了以上方法,还有一些可以在Windbg中直接查看Handle的方式来查找句柄泄露:

  1. 通过!handle命令查看当前进程的所有句柄
  2. 通过对比两个时间点的!handle命令的结果,找出句柄泄露的类型
  3. 找出两个时间点差异化的句柄的索引,再使用!handle <handle index> f查看句柄的详细信息。
  4. 通过泄露的句柄的类型,详细信息(比如名称)来辅助定位可能的句柄泄露位置

最后是个人微信公众号,文章CSDN和微信公众号都会发,欢迎一起讨论。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/CJF_iceKing/article/details/111768664
今日推荐