一:背景

1. 讲故事

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

二:如何洞察泄露

1. 一个测试小案例

在 windows 上gdi的句柄类型有很多,比如:pen,font,bitmap,device 等,具体可以网上搜一下,这里我就造一个 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. 记一次在forEach中使用aynac/await中的坑

    1.背景 在写一个对齐脚本时 发现下列问题 const timeList = await imageList.map( (item,index)=>{ return item.identify_ ...

  2. 一条SQL语句在MySQL中如何执行

    一条SQL语句在MySQL中如何执行 本篇文章会分析一个 sql 语句在 MySQL 中的执行流程,包括 sql 的查询在 MySQL 内部会怎么流转,sql 语句的更新是怎么完成的. 在分析之前我会 ...

  3. 学习docker看此文足以

    什么是 Docker Docker 最初是 dotCloud 公司创始人  在法国期间发起的一个公司内部项目,它是基于 dotCloud 公司多年云服务技术的一次革新,并于 ,主要项目代码在  上进行 ...

  4. 7.OAuth2

    1.近几天在学习OAuth2协议,实际开发中用的最多的就是授权码模式   2.OAuth2的授权码模式流程:首先是用户去访问资源服务器,服务器会给用户一个授权码:用户根据授权码去访问认证服务器,服务器 ...

  5. 三天吃透Redis八股文

    Redis连环40问,绝对够全! Redis是什么? Redis(Remote Dictionary Server)是一个使用 C 语言编写的,高性能非关系型的键值对数据库.与传统数据库不同的是,Re ...

  6. DevOps infra | 互联网、软件公司基础设施建设(基建)哪家强?

    国内公司普遍不注重基础设施建设,这也是可以理解的.吃饭都吃不饱,就别提什么荤素搭配,两菜一汤了.但也不能全说是这样,还是有很多公司投入大量的人力物力去做好公司的基建,比如很多阿里和美团的小伙伴对公司的 ...

  7. RFS[3]: No standby redo logfiles available for thread 1

    问题描述:备库恢复DG之后,mrp进程一直是wait_for_log,主库创建数据没有正常同步,只有在切换归档的时候备库才能同步主库数据 查看主库日志,主库RFS进程提示没有可用的standby re ...

  8. Linux(五)用户管理与文件权限

    1 常用的基本命令 Shell可以看作一个命令解释器,为我们提供一个交互式的文本控制台界面,可以通过终端控制台来输入命令,由shell进行解释并最终交给linux内核运行.可以看作用户和硬件的桥梁. ...

  9. Docker入门实践笔记-基本使用

    容器是一个系统中被隔离的特殊环境,进程可以在其中不受干扰地运行,使用Docker来实现容器化 容器化 运行容器时,要先拉取一个镜像(image),再通过这个镜像来启动容器: $ sudo docker ...

  10. Django笔记二十四之数据库函数之比较和转换函数

    本文首发于公众号:Hunter后端 原文链接:Django笔记二十四之数据库函数之比较和转换函数 这一篇笔记开始介绍几种数据库函数,以下是几种函数及其作用 Cast 转换类型 Coalesce 优先取 ...