乐者为王

Do one thing, and do it well.

使用WinDbg检测内存泄漏

英文原文:https://www.codeproject.com/Articles/31382/Memory-Leak-Detection-Using-WinDbg

简介

内存泄漏是一种费时的bug,通常由C++开发者制造。内存泄漏的检测经常是令人厌烦的。如果代码不是你写的,或者代码库相当巨大,事情就会变得很糟糕。

尽管市场上有可用的工具帮助你检测内存泄露,但这些工具大多数不是免费的。我发现WinDbg作为一个免费的强大的工具可以用来解决内存泄漏bug。至少,我们可以知道可能涉嫌引起内存泄漏的代码位置。COM接口泄漏不在这篇文章范围内。

WinDbg是来自Microsoft的一个强大的用户/内核空间调试器,它可以从这里下载和安装。

使用WinDbg

开始使用WinDbg:

  1. 配置符号文件路径指向Microsoft符号服务器SRV*d:\symbols*http://msdl.microsoft.com/download/symbols
  2. 将你的程序EXE/DLL的PDB(程序数据库)路径添加到符号文件路径中;
  3. 你还需要配置操作系统的标记去启用有内存泄漏的进程上的用户栈跟踪。这很简单,可以用gflags.exe做到。gflags.exe在WinDbg安装期间被安装。或者也可以通过命令行来完成,使用命令gflags.exe /i MemoryLeak.exe +ust即可。我的程序名是Test2.exe。因此,为了演示,需要用Test2.exe替换掉MemoryLeak.exe。下面的快照显示了应用Test2.exe的OS标记的设置。

一旦我们配置好WinDbg的符号文件路径,开启泄漏内存的进程,就可以把WinDbg挂载上去。挂载选项可以在WinDbg的File菜单下找到,或者可以使用F6快捷键启动。下面的快照显示了挂载界面:

WinDbg的!heap命令用来显示堆。在WinDbg帮助文件里有!heap的详尽文档。

我已经开发了一个泄漏内存的小程序,在后面也将使用这个程序来做演示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int _tmain(int argc, _TCHAR* argv[])
{
    while(1)
    {
        AllocateMemory();
    }
    return 0;
}
void AllocateMemory()
{
    int* a = new int[2000];
    ZeroMemory(a, 8000);
    Sleep(1);
}

上面的程序泄漏一个大小为2000*4字节的整型数组。

把WinDbg挂载到进程上后,执行命令!heap -s。-s代表概要。下面是泄漏进程上!heap -s的输出:

1
2
3
4
5
6
7
8
9
10
11
12
0:001> !heap -s
NtGlobalFlag enables following debugging aids for new heaps:
    validate parameters
    stack back traces
  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast
                    (k)     (k)    (k)     (k) length      blocks cont. heap
-----------------------------------------------------------------------------
   00150000 58000062    1024     12     12      1     1     1    0      0   L
   00250000 58001062      64     24     24     15     1     1    0      0   L
   00260000 58008060      64     12     12     10     1     1    0      0
   00330000 58001062   64576  47404  47404     13     4     1    0      0
-----------------------------------------------------------------------------

让进程执行一段时间,然后重新进入进程,再次执行!heap -s。下面显示的是该命令的输出:

1
2
3
4
5
6
7
8
9
10
11
12
0:001> !heap -s
NtGlobalFlag enables following debugging aids for new heaps:
   validate parameters
   stack back traces
   Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast
                     (k)     (k)    (k)     (k) length      blocks cont. heap
   -----------------------------------------------------------------------------
    00150000 58000062    1024     12     12      1     1     1    0      0   L
    00250000 58001062      64     24     24     15     1     1    0      0   L
    00260000 58008060      64     12     12     10     1     1    0      0
    00330000 58001062  261184 239484 239484     14     4     1    0      0
   -----------------------------------------------------------------------------

第11行显示了增长堆。上面的快照显示一个句柄为00330000的堆在增长。

执行!heap -stat -h 00330000。这个命令用来显示增长堆的堆统计。下面显示的是该命令的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0:001> !heap -stat -h 00330000
heap @ 00330000
group-by: TOTSIZE max-display: 20
    size     #blocks     total     ( %) (percent of total busy bytes)
    1f64 76c6 - e905f58  (99.99)
    1800 1 - 1800  (0.00)
    824 2 - 1048  (0.00)
    238 2 - 470  (0.00)
    244 1 - 244  (0.00)
    4c 5 - 17c  (0.00)
    b0 2 - 160  (0.00)
    86 2 - 10c  (0.00)
    50 3 - f0  (0.00)
    74 2 - e8  (0.00)
    38 4 - e0  (0.00)
    48 3 - d8  (0.00)
    c4 1 - c4  (0.00)
    62 2 - c4  (0.00)
    be 1 - be  (0.00)
    b8 1 - b8  (0.00)
    ae 1 - ae  (0.00)
    ac 1 - ac  (0.00)
    55 2 - aa  (0.00)
    a4 1 - a4  (0.00)

上面的快照显示大小为1f64的76c6个块被分配(第5行)。如此巨大的相同大小的块的数量让我们怀疑这些可能是泄漏块。其余的块分配没有在增长的块数字。

下一步是去获得这些块的地址。使用命令!heap -flt s 1f64。这个命令过滤出堆的所有其它块,并且显示大小为1f64的块的详细信息。

下面显示的是该命令的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
0:001> !heap -flt s 1f64
    _HEAP @ 150000
    _HEAP @ 250000
    _HEAP @ 260000
    _HEAP @ 330000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        003360e0 03f0 0000  [07]   003360e8    01f64 - (busy)
        00338060 03f0 03f0  [07]   00338068    01f64 - (busy)
        00339fe0 03f0 03f0  [07]   00339fe8    01f64 - (busy)
        0033bf60 03f0 03f0  [07]   0033bf68    01f64 - (busy)
        0033dee0 03f0 03f0  [07]   0033dee8    01f64 - (busy)
        01420040 03f0 03f0  [07]   01420048    01f64 - (busy)
        01421fc0 03f0 03f0  [07]   01421fc8    01f64 - (busy)
        01423f40 03f0 03f0  [07]   01423f48    01f64 - (busy)
        01425ec0 03f0 03f0  [07]   01425ec8    01f64 - (busy)
        01427e40 03f0 03f0  [07]   01427e48    01f64 - (busy)
        01429dc0 03f0 03f0  [07]   01429dc8    01f64 - (busy)
        0142bd40 03f0 03f0  [07]   0142bd48    01f64 - (busy)
        0142dcc0 03f0 03f0  [07]   0142dcc8    01f64 - (busy)
        0142fc40 03f0 03f0  [07]   0142fc48    01f64 - (busy)
        01431bc0 03f0 03f0  [07]   01431bc8    01f64 - (busy)
        01433b40 03f0 03f0  [07]   01433b48    01f64 - (busy)
        01435ac0 03f0 03f0  [07]   01435ac8    01f64 - (busy)
        01437a40 03f0 03f0  [07]   01437a48    01f64 - (busy)
        014399c0 03f0 03f0  [07]   014399c8    01f64 - (busy)
        0143b940 03f0 03f0  [07]   0143b948    01f64 - (busy)
        0143d8c0 03f0 03f0  [07]   0143d8c8    01f64 - (busy)
        0143f840 03f0 03f0  [07]   0143f848    01f64 - (busy)
        014417c0 03f0 03f0  [07]   014417c8    01f64 - (busy)
        01443740 03f0 03f0  [07]   01443748    01f64 - (busy)
        014456c0 03f0 03f0  [07]   014456c8    01f64 - (busy)
        01447640 03f0 03f0  [07]   01447648    01f64 - (busy)
        014495c0 03f0 03f0  [07]   014495c8    01f64 - (busy)
        0144b540 03f0 03f0  [07]   0144b548    01f64 - (busy)
        0144d4c0 03f0 03f0  [07]   0144d4c8    01f64 - (busy)
        0144f440 03f0 03f0  [07]   0144f448    01f64 - (busy)
        014513c0 03f0 03f0  [07]   014513c8    01f64 - (busy)
        01453340 03f0 03f0  [07]   01453348    01f64 - (busy)
        014552c0 03f0 03f0  [07]   014552c8    01f64 - (busy)
        01457240 03f0 03f0  [07]   01457248    01f64 - (busy)
        014591c0 03f0 03f0  [07]   014591c8    01f64 - (busy)
        0145b140 03f0 03f0  [07]   0145b148    01f64 - (busy)
        0145d0c0 03f0 03f0  [07]   0145d0c8    01f64 - (busy)
        0145f040 03f0 03f0  [07]   0145f048    01f64 - (busy)
        01460fc0 03f0 03f0  [07]   01460fc8    01f64 - (busy)
        01462f40 03f0 03f0  [07]   01462f48    01f64 - (busy)
        01464ec0 03f0 03f0  [07]   01464ec8    01f64 - (busy)
        01466e40 03f0 03f0  [07]   01466e48    01f64 - (busy)
        01468dc0 03f0 03f0  [07]   01468dc8    01f64 - (busy)

使用任何来自上面输出中的UsrPtr列值,然后使用命令!heap -p -a UsrPtr去显示UsrPtr的调用栈。我选择了第27行的0143d8c8。

执行!heap -p -a 0143d8c8,我们得到下面显示的调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0:001> !heap -p -a 0143d8c8
    address 0143d8c8 found in
    _HEAP @ 330000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        0143d8c0 03f0 0000  [07]   0143d8c8    01f64 - (busy)
        Trace: 0025
        7c96d6dc ntdll!RtlDebugAllocateHeap+0x000000e1
        7c949d18 ntdll!RtlAllocateHeapSlowly+0x00000044
        7c91b298 ntdll!RtlAllocateHeap+0x00000e64
        102c103e MSVCR90D!_heap_alloc_base+0x0000005e
        102cfd76 MSVCR90D!_heap_alloc_dbg_impl+0x000001f6
        102cfb2f MSVCR90D!_nh_malloc_dbg_impl+0x0000001f
        102cfadc MSVCR90D!_nh_malloc_dbg+0x0000002c
        102db25b MSVCR90D!malloc+0x0000001b
        102bd691 MSVCR90D!operator new+0x00000011
        102bd71f MSVCR90D!operator new[]+0x0000000f
        4113d8 Test2!AllocateMemory+0x00000028
        41145c Test2!wmain+0x0000002c
        411a08 Test2!__tmainCRTStartup+0x000001a8
        41184f Test2!wmainCRTStartup+0x0000000f
        7c816fd7 kernel32!BaseProcessStart+0x00000023

第16-19行显示了来自我们代码的函数。

注意:有时候,可能发生!heap -s命令不显示增长堆的情况。如果那样的话,使用!heap -stat -h命令列出带有堆的大小和块的数量的所有堆。识别在增长的块的数量,然后使用!heap -flt s SIZE(SIZE = 涉嫌块的大小)命令。

Comments