一:背景
1. 講故事
前段時間有位朋友找到我,說他的程序界面操作起來很慢并且卡頓等一些不正?,F(xiàn)象,從任務(wù)管理器看了下 GDI句柄
已經(jīng)到 1w 了,一時也找不出什么代碼中哪里有問題,讓我?guī)兔聪拢鋵嵾@種問題看內(nèi)存dump作用不是很大,主要是寫腳本很麻煩,這一篇我們就來簡單聊聊如何洞察此類問題。
二:如何洞察泄露
1. 一個測試小案例
在 windows 上gdi的句柄類型有很多,比如:pen
,font
,bitmap
,device
等,具體可以網(wǎng)上搜一下,這里我就造一個 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
附加進(jìn)程。
2. 如何觀察GDI泄露
觀察 GDI句柄
是否異常,最簡單的方法就是看任務(wù)管理器中的 GDI對象
一列,截圖如下:
但這里有一個問題,你只知道有一個總數(shù),并不知道是哪種句柄類型的泄露,比如是:bitmap? font ?device? 對吧。
那怎么辦呢?這就需要考驗一點基礎(chǔ)知識了,你要知道 GDI 的句柄表(GDI Shared Handle Table)是維護在用戶態(tài)的虛擬地址上,區(qū)別于維護在內(nèi)核中的 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
的虛擬地址段中就雪藏了我們要找的各句柄的統(tǒng)計信息,但要挖它需要寫腳本,再配合 GDICELL
結(jié)構(gòu)體,分組其中的 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
,大家可以網(wǎng)上搜一下。
打開 GDIView 之后,可以很清楚的看到 WindowsFormsApp1
程序中各個句柄的統(tǒng)計信息,并且 type=Bitmap
是非常可疑的,截圖如下:
知道了是 Bitmap
的句柄泄露,定位的范圍一下子就小了很多,長舒一口氣。
3. 如何尋找 Bitmap 的底層函數(shù)
熟悉 Windows 的朋友應(yīng)該都知道 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 了,功夫不負(fù)有心人,找到了一篇很老的文章:https://learn.microsoft.com/en-us/archive/msdn-magazine/2003/january/detect-and-plug-gdi-leaks-with-two-powerful-tools-for-windows-xp
從圖中看記載的非常詳細(xì),但我親自觀察下來有些方法找不到,所以只能做個參考吧,不過在 Windbg 中提供了一個非常好的 bm
命令,它可以對方法名進(jìn)行 模糊斷點
,比如 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";
...
天網(wǎng)恢恢,疏而不漏,肯定會命中其中一個的,接下來繼續(xù) g 讓程序跑起來,你會看到有大量的方法被命中,并且仔細(xì)觀察會有一個用戶態(tài)函數(shù) <button1_Click>b__1_0
,截圖如下:
此時這個托管函數(shù)就是重點懷疑對象,也就很輕松的找到問題之所在,有些朋友可能要問,這樣重復(fù)的信息是不是會很多,那當(dāng)然了,大家可以根據(jù)輸出信息做下一步的洞察,比如上面的 gdiplus!CopyOnWriteBitmap::CreateHBITMAP
函數(shù)會特別多,這時候可以重新 bp 來縮小范圍,對吧!參考代碼如下:文章來源:http://www.zghlxwxcb.cn/news/detail-479885.html
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
...
三:總結(jié)
說實話,找到程序的 GDI句柄泄露
的前因后果難度系數(shù)還是蠻高的,在沒有系統(tǒng)科學(xué)的工具和基礎(chǔ)知識之前,花費幾天的時間排查這個問題是很正常的,相信這篇文章給后來人少踩坑吧。文章來源地址http://www.zghlxwxcb.cn/news/detail-479885.html

到了這里,關(guān)于如何洞察 C# 程序的 GDI 句柄泄露的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!