- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
上个月我写过一篇 如何洞察 C# 程序的 GDI 句柄泄露 文章,当时用的是 GDIView + WinDbg 把问题搞定,前者用来定位泄露资源,后者用来定位泄露代码,后面有朋友反馈两个问题:
其实那篇文章也聊过,在 x64 或者 wow64 的程序里,在用户态内存段中有一个 GDI Shared Handle Table 句柄表,这个表中就统计了各自句柄类型的数量,如果能统计出来也就回答了上面的问题,对吧.
32bit 程序的 GDI Shared Handle Table 段是没有的,即 _PEB.GdiSharedHandleTable = NULL .
0:002> dt ntdll!_PEB GdiSharedHandleTable 01051000 +0x0f8 GdiSharedHandleTable : (null)
有了这些前置基础,接下来就可以开挖了.
为了方便测试,我来造一个 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 参数 。
这里我们采用 第一种方式 ,截图如下:
使用伪寄存器变量提取出 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 结构体,接下来怎么鉴别每一条记录上都是什么类型呢?其实这里是有枚举的.
即 GDIView 中的 红色一列 .
到这里我们可以通过肉眼观察 + F5 检索,可以清晰的看到1029 个句柄对象,其中 1028 个是 DC 对象,其实这就是我们泄露的,截图如下:
如果大家通读会发现这些都是固定步骤,完全可以写成比如 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句柄泄露的再反思的文章就讲到这里了,如果你想了解更多关于.NET程序的GDI句柄泄露的再反思的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我是 C 语言新手,我编写了这个 C 程序,让用户输入一年中的某一天,作为返回,程序将输出月份以及该月的哪一天。该程序运行良好,但我现在想简化该程序。我知道我需要一个循环,但我不知道如何去做。这是程序
我一直在努力找出我的代码有什么问题。这个想法是创建一个小的画图程序,并有红色、绿色、蓝色和清除按钮。我有我能想到的一切让它工作,但无法弄清楚代码有什么问题。程序打开,然后立即关闭。 import ja
我想安装screen,但是接下来我应该做什么? $ brew search screen imgur-screenshot screen
我有一个在服务器端工作的 UDP 套接字应用程序。为了测试服务器端,我编写了一个简单的 python 客户端程序,它发送消息“hello world how are you”。服务器随后应接收消息,将
我有一个 shell 脚本,它运行一个 Python 程序来预处理一些数据,然后运行一个 R 程序来执行一些长时间运行的任务。我正在学习使用 Docker 并且我一直在运行 FROM r-base:l
在 Linux 中。我有一个 c 程序,它读取一个 2048 字节的文本文件作为输入。我想从 Python 脚本启动 c 程序。我希望 Python 脚本将文本字符串作为参数传递给 c 程序,而不是将
前言 最近开始整理笔记里的库存草稿,本文是 23 年 5 月创建的了(因为中途转移到 onedrive,可能还不止) 网页调起电脑程序是经常用到的场景,比如百度网盘下载,加入 QQ 群之类的 我
对于一个类,我被要求编写一个 VHDL 程序,该程序接受两个整数输入 A 和 B,并用 A+B 替换 A,用 A-B 替换 B。我编写了以下程序和测试平台。它完成了实现和行为语法检查,但它不会模拟。尽
module Algorithm where import System.Random import Data.Maybe import Data.List type Atom = String ty
我想找到两个以上数字的最小公倍数 求给定N个数的最小公倍数的C++程序 最佳答案 int lcm(int a, int b) { return (a/gcd(a,b))*b; } 对于gcd,请查看
这个程序有错误。谁能解决这个问题? Error is :TempRecord already defines a member called 'this' with the same paramete
当我运行下面的程序时,我在 str1 和 str2 中得到了垃圾值。所以 #include #include #include using namespace std; int main() {
这是我的作业: 一对刚出生的兔子(一公一母)被放在田里。兔子在一个月大时可以交配,因此在第二个月的月底,每对兔子都会生出两对新兔子,然后死去。 注:在第0个月,有0对兔子。第 1 个月,有 1 对兔子
我编写了一个程序,通过对字母使用 switch 命令将十进制字符串转换为十六进制,但是如果我使用 char,该程序无法正常工作!没有 switch 我无法处理 9 以上的数字。我希望你能理解我,因为我
我是 C++ 新手(虽然我有一些 C 语言经验)和 MySQL,我正在尝试制作一个从 MySQL 读取数据库的程序,我一直在关注这个 tutorial但当我尝试“构建”解决方案时出现错误。 (我正在使
仍然是一个初学者,只是尝试使用 swift 中的一些基本函数。 有人能告诉我这段代码有什么问题吗? import UIKit var guessInt: Int var randomNum = arc
我正在用 C++11 编写一个函数,它采用 constant1 + constant2 形式的表达式并将它们折叠起来。 constant1 和 constant2 存储在 std::string 中,
我用 C++ 编写了这段代码,使用运算符重载对 2 个矩阵进行加法和乘法运算。当我执行代码时,它会在第 57 行和第 59 行产生错误,非法结构操作(两行都出现相同的错误)。请解释我的错误。提前致谢:
我是 C++ 的初学者,我想编写一个简单的程序来交换字符串中的两个字符。 例如;我们输入这个字符串:“EXAMPLE”,我们给它交换这两个字符:“E”和“A”,输出应该类似于“AXEMPLA”。 我在
我需要以下代码的帮助: 声明 3 个 double 类型变量,每个代表三角形的三个边中的一个。 提示用户为第一面输入一个值,然后 将用户的输入设置为您创建的代表三角形第一条边的变量。 将最后 2 个步
我是一名优秀的程序员,十分优秀!