使用 Windows 调试器查找 GDI 泄漏





5.00/5 (24投票s)
使用 Windows 调试器跟踪 GDI 泄漏
引言
本文将介绍如何使用 Windows 调试器查找和修复 GDI 句柄泄漏。Windows 调试器应该是最后的手段,首先在整个代码库中搜索 BeginPaint()/EndPaint()
,并检查这两个函数调用之间的 GDI 代码是否有未删除的句柄,然后将它们删除。
使用任务管理器,我们可以在“详细信息”选项卡页上添加“GDI 对象”列,以获取每个进程当前打开的 GDI 句柄的计数。一个进程最多可以打开 10000 个 GDI 句柄。所有进程的系统范围限制为 65535。右键单击标题以选择要显示的列。
勾选“GDI 对象”选项以将其添加到“详细信息”选项卡,然后关闭对话框。
在我的应用程序中,UI 语言更改 20 次会导致应用程序无响应,因为它达到了 10000 个打开的 GDI 句柄的限制。`BeginPaint()/EndPaint()` 之间创建的所有 GDI 对象都经过双重检查以确保已释放。显然,泄漏发生在其他地方。为了缩小泄漏的 GDI 对象类型,我们可以使用 GDIView(基于 UI)或 GDIInquiry.exe(基于控制台)。下面是 GDIInquiry.exe 在 UI 语言更改之前和之后的快照。
GDIInquiry.exe PID
GDIInquiry.exe 需要一个参数,即可以从任务管理器中检索到的进程 ID。
C:\temp>GDIInquiry.exe 7168
Bitmap:3
Brush:6
DeviceContext:20
Font:5
Palette:0
Pen:0
Region:5
Unknown:0
GDITotal:39
我们可以看到,每次语言更改时,字体对象的计数会增加 50
,而其他 GDI 对象保持不变。
C:\temp>GDIInquiry.exe 7168
Bitmap:3
Brush:6
DeviceContext:20
Font:55
Palette:0
Pen:0
Region:5
Unknown:0
GDITotal:89
在 MSDN 页面 上找到字体创建函数列表后,我从文件菜单将 WinDbg 附加到我的进程(使用其 PID),然后使用 bp
命令在这些函数上设置断点。供您参考,gdi32
是 DLL 的名称,不带文件扩展名。请注意,GDI 句柄也可以由位于 USER32.DLL 的函数返回,请务必查阅 MSDN。在本例中,我只关注 GDI32.DLL 中的字体创建函数。
bp gdi32!createfonta
bp gdi32!createfontw
bp gdi32!createfontindirecta
bp gdi32!createfontindirectw
但是 WinDbg 无法找到这些函数。然后我改用 x
命令,进行不区分大小写的搜索,查找以 createfont
开头的符号名称,并使用星号作为通配符。
x gdi32!createfont*
x
命令产生以下搜索结果。
00007ffc`481f1210 GDI32!CreateFontWStub (void)
00007ffc`481f1630 GDI32!CreateFontIndirectW (void)
00007ffc`481fcf30 GDI32!CreateFontIndirectAStub (CreateFontIndirectAStub)
00007ffc`481fce50 GDI32!CreateFontAStub (CreateFontAStub)
00007ffc`481f87a0 GDI32!CreateFontIndirectExA (CreateFontIndirectExA)
00007ffc`481f87c0 GDI32!CreateFontIndirectExW (CreateFontIndirectExW)
然后我继续在每一个函数上设置断点。可以在之后使用引号指定附加命令,该命令将在断点命中后执行。多个命令用分号分隔。"kp"
指示 WinDbg 显示调用堆栈和每个函数的参数。还有一个以大写 P 结尾的 kp
版本(kP
):主要区别是每个参数显示在一行上。
bp GDI32!CreateFontWStub "kp"
bp GDI32!CreateFontIndirectW "kp"
bp GDI32!CreateFontIndirectAStub "kp"
bp GDI32!CreateFontAStub "kp"
bp GDI32!CreateFontIndirectExA "kp"
bp GDI32!CreateFontIndirectExW "kp"
设置多个断点可能会很乏味。幸运的是,可以使用 bm
命令来减轻这项繁重的工作,该命令可以使用通配符一次性设置多个断点。
bm gdi32!createfont* "kp"
设置完所需的断点后,在 WinDbg 中键入 g
命令以恢复程序执行。调试完成后,键入 qd
以分离程序并退出 WinDbg。
g
然后继续执行会产生 GDI 泄漏的任何操作,在本例中是更改 UI 语言。请注意,您可能需要中断几次才能遇到真正的泄漏,即创建字体但未删除。很快,WinDbg 会在 CreateFontIndirectW
处中断,但我不能公开显示我的调用堆栈,取而代之的是,我将解释泄漏的原因:在我的 CEdit
派生类中,创建了一个 HFONT
成员,并使用 SetFont()
将其设置为当前字体,而没有在其析构函数中调用 DeleteObject()
。修复此泄漏后,我的字体计数稳定下来,但 GDIView 中的“所有 GDI”计数和任务管理器中的 GDI 对象计数在每次语言更改时仍然迅速增长。
GDIView 页面指出:注意:如果“所有 GDI”值增加,而其他 GDI 值没有泄漏,则表示您可能存在图标或光标创建泄漏(图标和光标在创建后未被销毁)。。因此,对所有图标和光标创建代码进行了审查。泄漏来自多次调用的 LoadImage
。选择的修复方法不是删除图标,而是将图标加载为共享(LR_SHARED
),这意味着只加载一个图标实例,并在应用程序运行时结束时释放。
WNDCLASSEXW wnd = {0};
....
wnd.hIconSm = (HICON)::LoadImage(thisInstance, MAKEINTRESOURCE(1),
IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR);
修复方法是将 LR_SHARED
添加到最后一个参数中进行按位 OR
运算。
wnd.hIconSm = (HICON)::LoadImage(thisInstance, MAKEINTRESOURCE(1),
IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR | LR_SHARED);
为什么不能使用 !htrace 命令?
读者可能想知道为什么我没有使用 !htrace
命令,该命令专门用于跟踪句柄泄漏。!htrace
仅适用于真正的内核句柄。您不能使用 !htrace
扩展命令来跟踪图形设备接口 (GDI) 句柄泄漏的源头。
结论
本文介绍了查找字体和图标泄漏的方法。另一种常见的 GDI 泄漏类型是位图,我没有涵盖。请确保从 USER32.DLL 中的 GetIconInfo()/GetIconInfoExW()/GetIconInfoExA()
返回的位图句柄已被删除。如果您发现本文有用,请考虑点赞以鼓励我撰写更多此类主题的文章。
历史
- 2023 年 4 月 25 日:添加了关于系统范围 GDI 句柄限制为 65535 的说明。
- 2020 年 6 月 23 日:首次发布