如何洞察 C# 程序的 GDI 句柄泄露

这篇具有很好参考价值的文章主要介绍了如何洞察 C# 程序的 GDI 句柄泄露。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一:背景

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对象 一列,截图如下:

如何洞察 C# 程序的 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 是非常可疑的,截图如下:

如何洞察 C# 程序的 GDI 句柄泄露

知道了是 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

如何洞察 C# 程序的 GDI 句柄泄露

从图中看记载的非常详细,但我亲自观察下来有些方法找不到,所以只能做个参考吧,不过在 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,截图如下:

如何洞察 C# 程序的 GDI 句柄泄露

此时这个托管函数就是重点怀疑对象,也就很轻松的找到问题之所在,有些朋友可能要问,这样重复的信息是不是会很多,那当然了,大家可以根据输出信息做下一步的洞察,比如上面的 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句柄泄露 的前因后果难度系数还是蛮高的,在没有系统科学的工具和基础知识之前,花费几天的时间排查这个问题是很正常的,相信这篇文章给后来人少踩坑吧。文章来源地址https://www.toymoban.com/news/detail-479885.html

如何洞察 C# 程序的 GDI 句柄泄露

到了这里,关于如何洞察 C# 程序的 GDI 句柄泄露的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 记一次奇怪的文件句柄泄露问题

    记录并分享一下最近工作中遇到的 Too many open files 异常的解决过程。 产品有个上传压缩包并导入配置信息到数据库中的功能,主要流程如下: 用户上传压缩包; 后端解压存放在临时目录,并返回列表给用户; 用户选择需要导入哪些信息; 后端按需插入数据库中,完成后删

    2024年02月05日
    浏览(52)
  • RK3588 MPP解码句柄泄露问题记录

    最近在用瑞芯微3588开发板做一个视频处理的项目,前两天拷机发生了闪退,弹出的问题是“打开文件过多”,经过初步排查定位到是MPP硬解码部分出的问题。 我的MPP解码部分主要用来读取网络相机rtsp流,主要参考了一个github项目GitHub - MUZLATAN/ffmpeg_rtsp_mpp: ffmpeg 拉取rtsp h264流

    2024年02月09日
    浏览(89)
  • PerfView专题 (第十五篇): 如何洞察 C# 中的慢速方法

    在 dump 分析旅程中,经常会遇到很多朋友反馈一类问题,比如: 方法平时都执行的特别快,但有时候会特别慢,怎么排查? 我的方法第一次执行特别慢,能看到慢在哪里吗? 相信有朋友肯定说,加些日志不就好了,大方向肯定是没问题的,但加日志的颗粒度会比较粗而且侵

    2024年02月16日
    浏览(55)
  • PerfView专题 (第十六篇): 如何洞察C#托管堆内存的 "黑洞现象"

    首先声明的是这个 黑洞 是我定义的术语,它是用来表示 内存吞噬 的一种现象,何为 内存吞噬 ,我们来看一张图。 从上面的 卦象图 来看,GCHeap 的 Allocated=852M 和 Committed=16.6G ,它们的差值就是 分配缓冲区=16G ,缓冲区的好处就是用空间换时间,弊端就是会实实在在的侵占内

    2024年02月16日
    浏览(53)
  • C# wpf 使用GDI+实现截屏

    第一章 使用GDI+实现截屏(本章) 第二章 使用DockPanel制作截屏框 第三章 实现截屏框热键截屏 第四章 实现截屏框实时截屏 第五章 使用ffmpeg命令行实现录屏 wpf做屏幕录制或者屏幕广播之类的功能时需要实现截屏,在C#中比较容易实现的截屏方法是使用GDI+,本文将展示使用G

    2024年02月07日
    浏览(37)
  • C# Windows API应用:获取桌面所有窗口句柄的方法

    C# Windows API应用:获取桌面所有窗口句柄的方法 在 C# 的 Windows 应用程序开发中,我们常常需要获取桌面上所有窗口的句柄,以便进行一些窗口管理或者后续操作。本文将介绍一种利用 Windows API 获取桌面所有窗口句柄的方法,并提供相应的源代码和描述。 在开始之前,我们需

    2024年02月05日
    浏览(50)
  • PerfView 洞察C#托管堆内存 "黑洞现象"

    首先声明的是这个 黑洞 是我定义的术语,它是用来表示 内存吞噬 的一种现象,何为 内存吞噬 ,我们来看一张图。 从上面的 卦象图 来看,GCHeap 的 Allocated=852M 和 Committed=16.6G ,它们的差值就是 分配缓冲区=16G ,缓冲区的好处就是用空间换时间,弊端就是会实实在在的侵占内

    2024年02月16日
    浏览(49)
  • c#获取句柄,并对第三方软件的输入框和按钮进行控制

    刚学了没多久,还有很多地方没有理解到位,还请大家指正。 当程序运行起来,就会显示在任务管理器中,软件中的每个事件,比如按钮,文本框等一些控件,每个事件都会产生一个句柄,句柄就是这些事件的标识符。如果拿到句柄就可以自己写软件对第三方软件进行控制。

    2024年02月16日
    浏览(31)
  • 移动应用数据安全性:如何防止应用程序被黑客攻击和数据泄露?

    在移动应用成为人们生活中不可或缺的一部分的今天,数据安全性已经成为一个非常重要的问题。随着黑客攻击和数据泄露事件的频繁发生,用户对于移动应用程序的信任度也在逐渐下降。本文将探讨移动应用数据安全性的重要性,并提供一些有效的技术措施来防止应用程序

    2024年02月08日
    浏览(58)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包