GDI对象泄漏问题排查分析
和其他内存对象一样,GDI对象也是一种内存对象,它也会占用相关内存,如果没有及时使用DeleteObject
等函数去删除释放GDI对象的话,会造成内存泄漏。
GDI对象造成的泄漏往往表现有如下现象:
- UI操作卡顿或者整个程序运行卡顿,或者程序直接卡死。
- 造成程序崩溃;有一些UI库在使用GDI对象的时候,并不判断分配的GDI对象是否成功(默认当作成功),当GDI泄漏之后,导致GDI对象无法分配,直接使用造成崩溃。
- UI显示异常;例如有一些软件容错性处理的不错,当GDI无法分配的时候,并不会对软件造成使用上的影响,只是UI资源加载异常。
本文我们来分析一下GDI对象造成的泄漏问题应该如何分析。
1. 技术概述
GDI对象也是一种内存资源,会消耗内存,在Windows下面有位图,画刷,DC等GDI对象,下面我们统计一下Windows的GID对象,如下:
GDI object | Creator function | Destroyer function |
---|---|---|
Bitmap | CreateBitmap, CreateBitmapIndirect, CreateCompatibleBitmap, CreateDIBitmap, CreateDIBSection, CreateDiscardableBitmap | DeleteObject |
Brush | CreateBrushIndirect, CreateDIBPatternBrush, CreateDIBPatternBrushPt, CreateHatchBrush, CreatePatternBrush, CreateSolidBrush | DeleteObject |
DC | CreateDC | DeleteDC, ReleaseDC |
Enhanced metafile | CreateEnhMetaFile | DeleteEnhMetaFile |
Enhanced-metafile DC | CreateEnhMetaFile | CloseEnhMetaFile |
Font | CreateFont, CreateFontIndirect | DeleteObject |
Memory DC | CreateCompatibleDC | DeleteDC |
Metafile | CreateMetaFile | DeleteMetaFile |
Metafile DC | CreateMetaFile | CloseMetaFile |
Palette | CreatePalette | DeleteObject |
Pen and extended pen | CreatePen, CreatePenIndirect, ExtCreatePen | DeleteObject |
Region | CombineRgn, CreateEllipticRgn, CreateEllipticRgnIndirect, CreatePolygonRgn, CreatePolyPolygonRgn, CreateRectRgn, CreateRectRgnIndirect, CreateRoundRectRgn, ExtCreateRegion, PathToRegion | DeleteObject |
当我们使用GDI创建函数创建GDI对象之后,但是并不使用销毁函数销毁其对象之后就会造成GDI内存泄漏,如下:
void GdiLeak()
{
HPEN hPen = NULL;
hPen = CreatePen(PS_SOLID,5,RGB(255,0,0));
//...
//DeleteObject(hPen);
//hPen = NULL;
}
如果我们平时忘记调用DeleteObject(hPen)
的时候,就会造成GDI对象被泄漏。
2. 关于GDI句柄
我们知道一个内核对象都是通过句柄来进行引用的,一个内核对象的句柄其实是内核数据结构的一个哈希表索引;那么GDI句柄是什么呢?可以参考Windows GDI句柄分析;在这一篇文章中,我们得知GDI句柄在单个进程中默认被限制在了10000个,这里我们做一个实验验证一下。
我们使用如下代码泄漏GDI对象:
void CGdiLeakDlg::OnBnClickedButtonPenLeak()
{
for (int i = 0; i < 5000; ++i)
{
HPEN hPen = NULL;
hPen = CreatePen(PS_SOLID, 5, RGB(255, 0, 0));
}
}
void CGdiLeakDlg::OnBnClickedButtonBrushLeak()
{
for (int i = 0; i < 5000; ++i)
{
HBRUSH hBrush = CreateSolidBrush(RGB(200, 200, 200));
}
}
然后使用ProcExp来查看进程的GDI句柄信息,如下:
我们可以看到GDI Handles默认值是:10000个,无法继续再次创建了;那么我们可以看一下是否是这样的,如下:
当我们再次改变窗口大小的时候,创建已经无法渲染了(应该是无法创建GDI资源了)。
3. GDI泄漏检测
当遇到(或者怀疑)GDI内存泄漏的时候,我们可以使用两个工具进行查看,其一是任务管理器(选择GDI对象列),如下:
其二是使用一个GDIView的工具,这一款工具专门用来查看进程GDI信息,例如我们可以看到GDI信息如下:
这个工具可以查看到所有GDI的具体信息,以及每个对象的句柄等有用信息。
当然我们使用其他工具也可以查看GDI的句柄信息,例如:ProcExp或者ProcessHacker以及其他ARK工具。
4. GDI泄漏定位
GDI泄漏的定位和内存以及其他内核对象,我们可以通过如下方法:
- 使用WinDBG动态调试。
- 借助检测工具。
这里我们分析一下WinDBG的方法,例如上述,我们可以看到很多的句柄都是泄漏的Pen对象,那么我们就可以针对创建Pen的函数添加断点:
0:004> x gdi32!CreatePen*
76666440 GDI32!CreatePenStub (_CreatePenStub@12)
76667380 GDI32!CreatePenIndirectStub (_CreatePenIndirectStub@4)
0:004> bm gdi32!CreatePen*
1: 76666440 @!"GDI32!CreatePenStub"
2: 76667380 @!"GDI32!CreatePenIndirectStub"
0:004> g
ModLoad: 755c0000 755d2000 C:\WINDOWS\SysWOW64\kernel.appcore.dll
ModLoad: 696a0000 69782000 C:\WINDOWS\SysWOW64\textinputframework.dll
ModLoad: 7a2e0000 7a3ab000 C:\WINDOWS\SysWOW64\CoreMessaging.dll
ModLoad: 7a3b0000 7a643000 C:\WINDOWS\SysWOW64\CoreUIComponents.dll
ModLoad: 74310000 743fb000 C:\WINDOWS\SysWOW64\wintypes.dll
ModLoad: 74290000 7429b000 C:\WINDOWS\SysWOW64\CRYPTBASE.DLL
ModLoad: 72e40000 72ed6000 C:\WINDOWS\SysWOW64\TextShaping.dll
Breakpoint 1 hit
然后我们就可以查看断点处的调用,就可以发现是哪里存在泄漏了,如下(指示了GdiLeak!CGdiLeakDlg::OnBnClickedButtonPenLeak
在调用CreatePen
可能导致泄漏):
0:000> kc
#
00 GDI32!CreatePenStub
01 GdiLeak!CGdiLeakDlg::OnBnClickedButtonPenLeak
02 GdiLeak!_AfxDispatchCmdMsg
03 GdiLeak!CCmdTarget::OnCmdMsg
04 GdiLeak!CDialog::OnCmdMsg
05 GdiLeak!CWnd::OnCommand
06 GdiLeak!CDialogEx::OnCommand
07 GdiLeak!CWnd::OnWndMsg
08 GdiLeak!CWnd::WindowProc
09 GdiLeak!AfxCallWndProc
0a GdiLeak!AfxWndProc
当然网上还有人提出了使用Memory Validator工具检测的方法,对于这个工具的使用可以参考https://www.softwareverify.com/blog/theres-more-than-one-way-to-leak-a-gdi-object/(不过这个工具并不是免费使用的)。
除了这些方法之外,还有https://github.com/Softanics/gdi-leaks也是为了解决GDI泄漏缩写的工具,我们可以利用这个工具,比较快速定位问题。
5. 总结
GDI对象作为一个Windows的内存资源,也占用其内存空间,并且需要用户自动管理和释放;为了降低GDI对象的资源泄漏,我们可以使用智能指针来管理GDI对象句柄。
当我们的程序出现:
- 程序崩溃。
- UI资源显示异常。
- 程序运行缓慢。
此时我们应当注意是否GDI资源出现问题,可以使用如上方式进行GDI检测和泄漏位置的定位。
6. 参考
关于GDI内存资源泄漏的其他参考,可以参见如下链接:
- https://learn.microsoft.com/en-us/archive/msdn-magazine/2001/march/resource-leaks-detecting-locating-and-repairing-your-leaky-gdi-code。
- https://www.deleaker.com/blog/2021/12/16/gdi-leaks-how-to-identify-and-fix-them/。
- https://learn.microsoft.com/en-us/archive/msdn-magazine/2003/january/detect-and-plug-gdi-leaks-with-two-powerful-tools-for-windows-xp。
- https://www.softwareverify.com/blog/theres-more-than-one-way-to-leak-a-gdi-object/。
- https://www.codeproject.com/Articles/5306193/Find-GDI-Leaks-With-Windows-Debugger。