一:背景

1. 讲故事

前段时间有位朋友找到我,说他的程序界面操作起来很慢并且卡顿等一些不正常现象,从任务管理器看了下 GDI句柄 已经到 1w 了,一时也找不出什么代码中哪里有问题,让我帮忙看下,其实这种问题看内存dump作用不是很大,主要是写脚本很麻烦,这一篇我们就来简单聊聊如何洞察此类问题。

二:如何洞察泄露

1. 一个测试小案例

在 windows 上gdi的句柄类型有很多,比如:penfontbitmapdevice 等,具体可以网上搜一下,这里我就造一个 bitmap 的句柄泄露,参考代码如下:


private void button1_Click(object sender, EventArgs e)
{
Task.Factory.StartNew(() =>
{
Bitmap bmp = new Bitmap(100, 100); for (int i = 0; i < 10000; i++)
{
bmp.GetHbitmap();
Thread.Sleep(100);
}
});
}

代码非常简单,大概 100ms 泄露一个 bitmap 句柄,接下来把程序跑起来点击 Button_Click 按钮,然后上瑞士军刀 WinDbg 附加进程。

2. 如何观察GDI泄露

观察 GDI句柄 是否异常,最简单的方法就是看任务管理器中的 GDI对象 一列,截图如下:

但这里有一个问题,你只知道有一个总数,并不知道是哪种句柄类型的泄露,比如是:bitmap? font ?device? 对吧。

那怎么办呢?这就需要考验一点基础知识了,你要知道 GDI 的句柄表(GDI Shared Handle Table)是维护在用户态的虚拟地址上,区别于维护在内核中的 ObjectTable,可以用 !address 验证下。

0:011> !address 

        BaseAddress      EndAddress+1        RegionSize     Type       State                 Protect             Usage
--------------------------------------------------------------------------------------------------------------------------
+ 294`d1500000 294`d1681000 0`00181000 MEM_MAPPED MEM_COMMIT PAGE_READONLY Other [GDI Shared Handle Table] 0:011> !address 294`d1500000 Usage: Other
Base Address: 00000294`d1500000
End Address: 00000294`d1681000
Region Size: 00000000`00181000 ( 1.504 MB)
State: 00001000 MEM_COMMIT
Protect: 00000002 PAGE_READONLY
Type: 00040000 MEM_MAPPED
Allocation Base: 00000294`d1500000
Allocation Protect: 00000002 PAGE_READONLY
Additional info: GDI Shared Handle Table Content source: 1 (target), length: 181000

在这 1.5M虚拟地址段中就雪藏了我们要找的各句柄的统计信息,但要挖它需要写脚本,再配合 GDICELL 结构体,分组其中的 wType 句柄类型。


typedef struct {
PVOID64 pKernelAddress; // 0x00
USHORT wProcessId; // 0x08
USHORT wCount; // 0x0a
USHORT wUpper; // 0x0c
USHORT wType; // 0x0e
PVOID64 pUserAddress; // 0x10
} GDICell; // sizeof = 0x18

虽然可以手工分组出来,但这种问题你肯定不是第一个遇到,早有人写了一个工具来解决这类问题,它就是 GDIView.exe,大家可以网上搜一下。

打开 GDIView 之后,可以很清楚的看到 WindowsFormsApp1 程序中各个句柄的统计信息,并且 type=Bitmap 是非常可疑的,截图如下:

知道了是 Bitmap 的句柄泄露,定位的范围一下子就小了很多,长舒一口气。

3. 如何寻找 Bitmap 的底层函数

熟悉 Windows 的朋友应该都知道 GDI 的逻辑是封装在底层的 GDI32.dll 中,模块信息如下:


0:012> lmvm gdi32
Browse full module list
start end module name
00007ff9`b0c80000 00007ff9`b0cab000 GDI32 (deferred)
Image path: C:\windows\System32\GDI32.dll
Image name: GDI32.dll
Browse all global symbols functions data
Image was built with /Brepro flag.
Timestamp: 3EE1D71F (This is a reproducible build file hash, not a timestamp)
CheckSum: 0002B228
ImageSize: 0002B000
File version: 10.0.19041.2130
Product version: 10.0.19041.2130
File flags: 0 (Mask 3F)
File OS: 40004 NT Win32
File type: 2.0 Dll
File date: 00000000.00000000
Translations: 0409.04b0
Information from resource tables:
CompanyName: Microsoft Corporation
ProductName: Microsoft Windows Operating System
InternalName: gdi32
OriginalFilename: gdi32
ProductVersion: 10.0.19041.2130
FileVersion: 10.0.19041.2130 (WinBuild.160101.0800)
FileDescription: GDI Client DLL
LegalCopyright: Microsoft Corporation. All rights reserved.

言外之意就是可以在 GDI32 模块中下方法断点,这时候问题就来了,到底搁哪个方法下呢?这个只能求助 MSDN 了,功夫不负有心人,找到了一篇很老的文章:https://learn.microsoft.com/en-us/archive/msdn-magazine/2003/january/detect-and-plug-gdi-leaks-with-two-powerful-tools-for-windows-xp

从图中看记载的非常详细,但我亲自观察下来有些方法找不到,所以只能做个参考吧,不过在 Windbg 中提供了一个非常好的 bm 命令,它可以对方法名进行 模糊断点,比如 bm gdi32!*Bitmap* 就可以一口气下 45 个断点。


0:012> bm gdi32!*Bitmap* "? @$tid; k; gc"
0: 00007ff9`b0c86f7c @!"GDI32!IsCreateBitmapPresent"
1: 00007ff9`b0c87216 @!"GDI32!_imp_load_CreateDIBitmap"
2: 00007ff9`b0c8906c @!"GDI32!_imp_load_DwmCreatedBitmapRemotingOutput"
3: 00007ff9`b0c86460 @!"GDI32!NtGdiGetBitmapDpiScaleValue"
4: 00007ff9`b0c8850c @!"GDI32!_imp_load_ClearBitmapAttributes"
5: 00007ff9`b0c88745 @!"GDI32!_imp_load_CreateDiscardableBitmap"
6: 00007ff9`b0c84470 @!"GDI32!CreateBitmapStub"
...
42: 00007ff9`b0c8713e @!"GDI32!_imp_load_GetBitmapBits"
43: 00007ff9`b0c89580 @!"GDI32!GdiConvertBitmapV5"
44: 00007ff9`b0c89080 @!"GDI32!DwmCreatedBitmapRemotingOutput"
45: 00007ff9`b0c8aaac @!"GDI32!_imp_load_SetBitmapDimensionEx" 0:007> .bpcmds
bu0 @!"GDI32!IsCreateCompatibleBitmapPresent" "? @$tid; k; gc";
bu1 @!"GDI32!_imp_load_CreateDIBitmap" "? @$tid; k; gc";
bu2 @!"GDI32!_imp_load_DwmCreatedBitmapRemotingOutput" "? @$tid; k; gc";
bu3 @!"GDI32!NtGdiGetBitmapDpiScaleValue" "? @$tid; k; gc";
bu4 @!"GDI32!_imp_load_ClearBitmapAttributes" "? @$tid; k; gc";
bu5 @!"GDI32!_imp_load_CreateDiscardableBitmap" "? @$tid; k; gc";
...

天网恢恢,疏而不漏,肯定会命中其中一个的,接下来继续 g 让程序跑起来,你会看到有大量的方法被命中,并且仔细观察会有一个用户态函数 <button1_Click>b__1_0,截图如下:

此时这个托管函数就是重点怀疑对象,也就很轻松的找到问题之所在,有些朋友可能要问,这样重复的信息是不是会很多,那当然了,大家可以根据输出信息做下一步的洞察,比如上面的 gdiplus!CopyOnWriteBitmap::CreateHBITMAP 函数会特别多,这时候可以重新 bp 来缩小范围,对吧!参考代码如下:


0:010> bc *
0:010> bp gdiplus!CopyOnWriteBitmap::CreateHBITMAP "? @$tid; k; gc" 0:010> g
Evaluate expression: 15768 = 00000000`00003d98
# Child-SP RetAddr Call Site
00 000000bb`041febd8 00007ff9`9df0a21f gdiplus!CopyOnWriteBitmap::CreateHBITMAP
01 000000bb`041febe0 00007ff9`9df0a19a gdiplus!GpBitmap::CreateHBITMAP+0x3b
02 000000bb`041fec10 00007ff9`72442c61 gdiplus!GdipCreateHBITMAPFromBitmap+0xaa
03 000000bb`041fec50 00007ff9`72439471 System_Drawing_ni+0x72c61
04 000000bb`041fed10 00007ff9`7243940a System_Drawing_ni!System.Drawing.Bitmap.GetHbitmap+0x51
05 000000bb`041fed70 00007ff9`36d02a75 System_Drawing_ni!System.Drawing.Bitmap.GetHbitmap+0x7a
06 000000bb`041fede0 00007ff9`8d597a47 WindowsFormsApp1!WindowsFormsApp1.Form1.<>c.<button1_Click>b__1_0+0x75
...

三:总结

说实话,找到程序的 GDI句柄泄露 的前因后果难度系数还是蛮高的,在没有系统科学的工具和基础知识之前,花费几天的时间排查这个问题是很正常的,相信这篇文章给后来人少踩坑吧。

如何洞察 C# 程序的 GDI 句柄泄露的更多相关文章

  1. uilib库gdi句柄泄漏bug修复,duilib防止gdi泄漏的小提醒

    转载请说明原出处,谢谢~~ 今天下午群友的网友让我帮忙看一下的duilib程序的问题,程序中包含了List控件,会定时清除所有子项目然后重新添加.但是程序运行一段时间后会自己崩溃!我编译了源码运行后在 ...

  2. .NET对象与Windows句柄(二):句柄分类和.NET句柄泄露的例子

    上一篇文章介绍了句柄的基本概念,也描述了C#中创建文件句柄的过程.我们已经知道句柄代表Windows内部对象,文件对象就是其中一种,但显然系统中还有更多其它类型的对象.本文将简单介绍Windows对象 ...

  3. .NET对象与Windows句柄(三):句柄泄露实例分析

    在上篇文章.NET对象与Windows句柄(二):句柄分类和.NET句柄泄露的例子中,我们有一个句柄泄露的例子.例子中多次创建和Dispose了DataReceiver和DataAnalyzer对象, ...

  4. 查GDI对象泄露的利器:GDIView

    查GDI对象泄露的利器:GDIView可以很详细的查到进程的GDI对象的总个数,详细的GDI对象的个数,以及其增减数量.其GDI对象类型也可以很详细的得知,以及其内存地址,句柄.实在是好使! 下载地址 ...

  5. 【转】Graphics.DrawImage 方法 IntPtr 结构 GDI 句柄 知识收集

    Graphics.DrawImage 方法 在指定的位置使用原始物理大小绘制指定的 Image. 命名空间:System.Drawing 程序集:System.Drawing(在 system.dra ...

  6. windbg调试实例(4)--句柄泄露

    同事介绍了一篇调试句柄泄露的blog文章,今天有空看了一下,这家伙用视频的方式录下整个调试的过程,学习一目了然,真是有心.鉴于学习的过程总结一下能加深记忆,所以我这里做个记录,感兴趣的朋友可以看这里: ...

  7. linux句柄泄露问题查看

    背景: 我们在开发linux在线server的时候常常会遇会句柄泄露的问题.由于在linux系统设计里面遵循一切都是文件的原则.即磁盘文件.文件夹.网络套接字.磁盘.管道等,全部这些都是文件.在我们进 ...

  8. 捉虫记2:windows程序句柄泄露的上下文环境

    作为程序员,开发程序是基本功,而调试程序也是必不可少的技能之一.软件在主体功能开发完成后会经历各个阶段的测试,才会被发布.在测试过程中,出现较多的可能就是内存泄漏,句柄泄漏,异常崩溃等属于非功能型的软 ...

  9. SOCKET句柄泄露带来的内存灾难

    前些时候游戏莫名其妙出现大量内存泄露,我感到很诧异,当然一般情况下游戏的内存管理是极其严苛的,出现如此大量的内存泄露到底是怎么回事? 句柄滥用导致的内存泄露会多夸张呢,尤其SOCKET,在某些客户端系 ...

  10. [Swift通天遁地]七、数据与安全-(11)如何检测应用程序中的内存泄露

    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★➤微信公众号:山青咏芝(shanqingyongzhi)➤博客园地址:山青咏芝(https://www.cnblogs. ...

随机推荐

  1. Trie树结构

    PrefixTree 208. 实现 Trie (前缀树) Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键.这一数据结构 ...

  2. TypeScript 学习总结

    TypeScript JavaScript 语言 面向对象编程语言 面向脚本编程 是否支持可选参数 支持 不支持 是否支持静态类型 支持 不支持 是否支持接口 支持 不支持 TS:是JS的超集,即对J ...

  3. offsetX与offsetLeft

    offsetX:鼠标指针距离当前绑定元素左侧距离,他并不是相对于带有定位的父盒子的x,y坐标, 记住了,很多博客都解释错了 offsetLeft,offsetTop 相对于最近的祖先定位元素.

  4. JavaScript中计时器requestAnimationFrame、setTimeout、setInterval、setImmediate的使用和区别

    在JavaScript中,我们经常使用requestAnimationFrame.setTimeout.setInterval和setImmediate来控制代码的执行时机.它们各有特点和适用场景: ...

  5. Wiki.js配置LDAP认证

    安装好wikijs 之后, 可以进行进一步的详细配置. 这里介绍 LDAP 认证的配置. 在 管理 -> 身份验证 -> 添加策略 -> 选择 LDAP/AD , 如下: 接下来进行 ...

  6. pysimplegui之运行多个窗口

    运行多个窗口 这就是 PySimpleGUI 继续简单的地方,但问题空间刚刚进入"复杂"领域. 如果您希望在事件循环中运行多个窗口,那么有两种方法可以做到这一点. 当第二个窗口可见 ...

  7. EF Core 使用Azure App Service中的In-App MySQL服务

    Azure App Service 提供了一个应用内的MySQL,可以供测试.开发使用. 前提条件是需要使用Windows的操作系统. 创建完App Server 之后,只需要在设置下开启 MySQL ...

  8. C# System.lnvalidOperationException:"A second operation started on this context before a previousoperation completed. This is usually caused by different threads using the same instance ofDbContext...

    与上一篇问题一样,只是错误不一样,DbContext 不支持并发请求,每个数据库操作都使用await就可以了

  9. 【c#表达式树】最完善的表达式树Expression.Dynamic的玩法

    引言 在我第一次写博客的时候,写的第一篇文章,就是关于表达式树的,链接:https://www.cnblogs.com/1996-Chinese-Chen/p/14987967.html,其中,当时一 ...

  10. 是时候,升级你的 Windows 了「GitHub 热点速览」

    不知道多少小伙伴用着 Windows 操作系统,可能会有一个烦恼是有时候操作系统过慢,因为众多拖慢 Windows 系统的组件.Atlas 作为一个修改版的 Windows 系统,能极大提高操作系统运 ...