65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Windows 调试器查找 GDI 泄漏

starIconstarIconstarIconstarIconstarIcon

5.00/5 (24投票s)

2021 年 6 月 23 日

MIT

4分钟阅读

viewsIcon

17741

downloadIcon

377

使用 Windows 调试器跟踪 GDI 泄漏

引言

本文将介绍如何使用 Windows 调试器查找和修复 GDI 句柄泄漏。Windows 调试器应该是最后的手段,首先在整个代码库中搜索 BeginPaint()/EndPaint(),并检查这两个函数调用之间的 GDI 代码是否有未删除的句柄,然后将它们删除。

使用任务管理器,我们可以在“详细信息”选项卡页上添加“GDI 对象”列,以获取每个进程当前打开的 GDI 句柄的计数。一个进程最多可以打开 10000 个 GDI 句柄。所有进程的系统范围限制为 65535。右键单击标题以选择要显示的列。

Select Columns on Details Tab on Task Manager

勾选“GDI 对象”选项以将其添加到“详细信息”选项卡,然后关闭对话框。

Check GDI Objects option

在我的应用程序中,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 日:首次发布
© . All rights reserved.