暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

句柄泄露问题追踪

一个程序员的修炼之路 2020-12-27
1016

无论是在编写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. 通过泄露的句柄的类型,详细信息(比如名称)来辅助定位可能的句柄泄露位置


        文章转载自一个程序员的修炼之路,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

        评论