一:背景

1. 讲故事

上个月我写过一篇 如何洞察 C# 程序的 GDI 句柄泄露 文章,当时用的是 GDIView + WinDbg 把问题搞定,前者用来定位泄露资源,后者用来定位泄露代码,后面有朋友反馈两个问题:

  • GDIView 统计不准怎么办?
  • 我只有 Dump 可以统计吗?

其实那篇文章也聊过,在 x64 或者 wow64 的程序里,在用户态内存段中有一个 GDI Shared Handle Table 句柄表,这个表中就统计了各自句柄类型的数量,如果能统计出来也就回答了上面的问题,对吧。

32bit 程序的 GDI Shared Handle Table 段是没有的,即 _PEB.GdiSharedHandleTable = NULL


0:002> dt ntdll!_PEB GdiSharedHandleTable 01051000
+0x0f8 GdiSharedHandleTable : (null)

有了这些前置基础,接下来就可以开挖了。

二:挖 GdiSharedHandleTable

1. 测试代码

为了方便测试,我来造一个 DC句柄 的泄露。


internal class Program
{ [DllImport("Example_20_1_5", CallingConvention = CallingConvention.Cdecl)]
extern static void GDILeak(); static void Main(string[] args)
{
try
{
GDILeak();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
} Console.ReadLine();
}
}

然后就是 GDILeak 的 C++ 实现代码。


extern "C"
{
_declspec(dllexport) void GDILeak();
} void GDILeak()
{
while (true)
{
CreateDCW(L"DISPLAY", nullptr, nullptr, nullptr); auto const gdiObjectsCount = GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS);
std::cout << "GDI objects: " << gdiObjectsCount << std::endl; Sleep(10);
}
}

程序跑起来后,如果你是x64的程序那没有关系,但如果你是 32bit 的程序一定要生成一个 Wow64 格式的 Dump,千万不要抓它的 32bit dump,否则拿不到 GdiSharedHandleTable 字段也就无法后续分析了,那如何生成 Wow64 格式的呢?我推荐两种方式。

  • 使用64bit任务管理器(系统默认)生成

  • 使用 procdump -64 -ma QQ.exe 中的 -64 参数

这里我们采用第一种方式,截图如下:

2. 分析 GdiSharedHandleTable

使用伪寄存器变量提取出 GdiSharedHandleTable 字段,输出如下:


0:000> dt ntdll!_PEB GdiSharedHandleTable @$peb
+0x0f8 GdiSharedHandleTable : 0x00000000`03560000 Void

接下来使用 !address 找到这个 GdiSharedHandleTable 的首末地址。


0:000> !address 0x00000000`03560000 Usage: Other
Base Address: 00000000`03560000
End Address: 00000000`036e1000
Region Size: 00000000`00181000 ( 1.504 MB)
State: 00001000 MEM_COMMIT
Protect: 00000002 PAGE_READONLY
Type: 00040000 MEM_MAPPED
Allocation Base: 00000000`03560000
Allocation Protect: 00000002 PAGE_READONLY
Additional info: GDI Shared Handle Table Content source: 1 (target), length: 181000

上一篇我们聊过每新增一个GDI句柄都会在这个表中增加一条 GDICell,输出如下:


typedef struct {
PVOID64 pKernelAddress;
USHORT wProcessId;
USHORT wCount;
USHORT wUpper;
USHORT wType;
PVOID64 pUserAddress;
} GDICell;

这个 GDICell 有两个信息比较重要。

  • wProcessId 表示进程 ID
  • wType 表示句柄类型。

理想情况下是对 句柄类型 进行分组统计就能知道是哪里的泄露,接下来的问题是如何找呢?可以仔细观察结构体, wProcessId 和 wType 的偏移是 3USHORT=6byte,我们在内存中找相对偏移不就可以了吗?接下来在内存中搜索这块


0:000> ~.
. 0 Id: 101c.4310 Suspend: 0 Teb: 00000000`009bf000 Unfrozen
Start: Example_20_1_4_exe!wmainCRTStartup (00000000`00d4ffe0)
Priority: 0 Priority class: 32 Affinity: fff 0:000> s-w 03560000 036e1000 101c
00000000`03562060 101c 0000 af01 0401 0b00 0830 0000 0000 ..........0.....
00000000`035782a0 101c ff1d ffff ffff 0000 0000 1d0f 010f ................
00000000`0357c688 101c 0000 3401 0401 0160 0847 0000 0000 .....4..`.G.....
...
00000000`035a5f98 101c 0000 0801 0401 0dc0 08a1 0000 0000 ................
00000000`035a5fb0 101c 0000 0801 0401 0c60 08a1 0000 0000 ........`.......
00000000`035a5fc8 101c 0000 0801 0401 0840 08a1 0000 0000 ........@.......
00000000`035a5fe0 101c 0000 0801 0401 0b00 08a1 0000 0000 ................

从卦中可以看到,当前有1029个 GDICell 结构体,接下来怎么鉴别每一条记录上都是什么类型呢?其实这里是有枚举的。

  1. DC = 0x01
  2. Region = 0x04
  3. Bitmap = 0x05
  4. Palette =0x08
  5. Font =0x0a
  6. Brush = 0x10
  7. Pen = 0x30

即 GDIView 中的 红色一列

到这里我们可以通过肉眼观察 + F5 检索,可以清晰的看到1029 个句柄对象,其中 1028 个是 DC 对象,其实这就是我们泄露的,截图如下:

3. 脚本处理

如果大家通读会发现这些都是固定步骤,完全可以写成比如 C++ 和 Javascript 的格式脚本,在 StackOverflow 上还真有这样的脚本。


$$ Run as: $$>a<DumpGdi.txt
$$ Written by Alois Kraus 2016
$$ uses pseudo registers r0-5 and r8-r14 r @$t1=0
r @$t8=0
r @$t9=0
r @$t10=0
r @$t11=0
r @$t12=0
r @$t13=0
r @$t14=0
$$ Increment count is 1 byte until we find a matching field with the current pid
r @$t4=1 r @$t0=$peb
$$ Get address of GDI handle table into t5
.foreach /pS 3 /ps 1 ( @$GdiSharedHandleTable { dt ntdll!_PEB GdiSharedHandleTable @$t0 } ) { r @$t5 = @$GdiSharedHandleTable } $$ On first call !address produces more output. Do a warmup
.foreach /pS 50 ( @$myStartAddress {!address @$t5} ) { } $$ Get start address of file mapping into t2
.foreach /pS 4 /ps 40 ( @$myStartAddress {!address @$t5} ) { r @$t2 = @$myStartAddress }
$$ Get end address of file mapping into t3
.foreach /pS 7 /ps 40 ( @$myEndAddress {!address @$t5} ) { r @$t3 = @$myEndAddress }
.printf "GDI Handle Table %p %p", @$t2, @$t3 .for(; @$t2 < @$t3; r @$t2 = @$t2 + @$t4)
{
$$ since we walk bytewise through potentially invalid memory we need first to check if it points to valid memory
.if($vvalid(@$t2,4) == 1 )
{
$$ Check if pid matches
.if (wo(@$t2) == @$tpid )
{
$$ increase handle count stored in $t1 and increase step size by 0x18 because we know the cell structure GDICell has a size of 0x18 bytes.
r @$t1 = @$t1+1
r @$t4 = 0x18
$$ Access wType of GDICELL and increment per GDI handle type
.if (by(@$t2+6) == 0x1 ) { r @$t8 = @$t8+1 }
.if (by(@$t2+6) == 0x4 ) { r @$t9 = @$t9+1 }
.if (by(@$t2+6) == 0x5 ) { r @$t10 = @$t10+1 }
.if (by(@$t2+6) == 0x8 ) { r @$t11 = @$t11+1 }
.if (by(@$t2+6) == 0xa ) { r @$t12 = @$t12+1 }
.if (by(@$t2+6) == 0x10 ) { r @$t13 = @$t13+1 }
.if (by(@$t2+6) == 0x30 ) { r @$t14 = @$t14+1 }
}
}
} .printf "\nGDI Handle Count %d", @$t1
.printf "\n\tDeviceContexts: %d", @$t8
.printf "\n\tRegions: %d", @$t9
.printf "\n\tBitmaps: %d", @$t10
.printf "\n\tPalettes: %d", @$t11
.printf "\n\tFonts: %d", @$t12
.printf "\n\tBrushes: %d", @$t13
.printf "\n\tPens: %d", @$t14
.printf "\n\tUncategorized: %d\n", @$t1-(@$t14+@$t13+@$t12+@$t11+@$t10+@$t9+@$t8)

最后我们用脚本跑一下,哈哈,是不是非常清楚。


0:000> $$>a< "D:\testdump\DumpGdi.txt"
GDI Handle Table 0000000003560000 00000000036e1000
GDI Handle Count 1028
DeviceContexts: 1028
Regions: 0
Bitmaps: 0
Palettes: 0
Fonts: 0
Brushes: 0
Pens: 0
Uncategorized: 0

三:总结

如果大家想从 DUMP 文件中提取 GDI 句柄泄露类型,这是一篇很好的参考资料,相信能从另一个角度给你提供一些灵感。

.NET程序的 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. 【转】Graphics.DrawImage 方法 IntPtr 结构 GDI 句柄 知识收集

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

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

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

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

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

  7. linux句柄泄露问题查看

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

  8. Android 在安装完成界面,点击打开应用程序。在应用程序点击home键,再从桌面打开程序导致产生多个实例或者说程序被重复打开

    Android 在安装完成界面,点击打开应用程序.在应用程序点击home键,再从桌面打开程序导致产生多个实例或者说程序被重复打开. etong_123的专栏 - 博客频道 - CSDN.NET htt ...

  9. 联想ERP项目实施案例分析(10):回到最初再反思IT价值

    联想ERP项目实施案例分析(10):回到最初再反思IT价值 投入上千万(未来每年的维护费也非常高),投入一年实施时间,高级副总裁亲自挂帅,各级业务部门管理者亲自负责.骨干业务人员充当区域IT实施者/推 ...

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

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

随机推荐

  1. VS2022使用ClickOnce发布程序本地安装.net框架

    因为遇到下面的错误,没有在网上搜到详细解决问题的教程,费了一些时间才解决了问题,特此记录一下,也希望能帮助到其他人. 要在"系统必备"对话框中启用"从与我的应用程序相同的 ...

  2. git拉取代码总提示输入密码解决方法

    公司用的gitlab,在项目拉取(git clone)和更新(git pull)的时候,每次都提示输入用户名密码,不胜其烦,解决方法如下: # 首先 git config --global crede ...

  3. IIS6网站批量迁移至IIS7经验分享

    迁移原因:公司服务器更换 迁移环境:源服务器 windows2003 X86   IIS6        目标服务器:windows2008 X64  IIS7 迁移过程: 第一次迁移失败,作为简要记 ...

  4. Golang for循环遍历小坑

    一.for循环 循环:让程序多次执行相同的代码块for循环是Go语言中唯一一个循环结构for循环经典语法先执行表达式1执行表达式2判断是否成立,如果成立执行循环体循环体执行完成后,执行表达式3再次执行 ...

  5. 大家都在用-神奇的Markdown格式

    概述 Markdown 是一种轻量级标记语言,它可以使我们专注于写作内容,而不用过多关注排版,很多博主.作家等都用它来撰写文章~ 本文将给各位小伙伴介绍 Markdown 语法的使用,本篇文章索奇就是 ...

  6. 2022-01-29:连接词。 给你一个 不含重复 单词的字符串数组 words ,请你找出并返回 words 中的所有 连接词 。 连接词 定义为:一个完全由给定数组中的至少两个较短单词组成的字符串

    2022-01-29:连接词. 给你一个 不含重复 单词的字符串数组 words ,请你找出并返回 words 中的所有 连接词 . 连接词 定义为:一个完全由给定数组中的至少两个较短单词组成的字符串 ...

  7. 2021-10-02:单词搜索。给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。单词必须按照字母

    2021-10-02:单词搜索.给定一个 m x n 二维字符网格 board 和一个字符串单词 word .如果 word 存在于网格中,返回 true :否则,返回 false .单词必须按照字母 ...

  8. Spring Cloud开发实践(六): 基于Consul和Spring Cloud 2021.0的演示项目

    目录 Spring Cloud开发实践(一): 简介和根模块 Spring Cloud开发实践(二): Eureka服务和接口定义 Spring Cloud开发实践(三): 接口实现和下游调用 Spr ...

  9. C++实现查询本机信息并且上报

    业务需求 共享文件夹.盘会导致系统安全性下降,故IT部门需要搜集公司中每台电脑的共享情况,并且进行上报 关键字 WMI查询.Get请求.C++网络库mongoose 前置需要 1.简单C++语法知识2 ...

  10. Singleton 单例模式简介与 C# 示例【创建型】【设计模式来了】

    〇.简介 1.什么是单例模式? 一句话解释:   单一的类,只能自己来创建唯一的一个对象. 单例模式(Singleton Pattern)是日常开发中最简单的设计模式之一.这种类型的设计模式属于创建型 ...