[翻譯]查詢Windows記憶體洩露的幾種方法

阿東發表於2018-04-20

筆者:

Windows記憶體洩露問題和很多二進位制漏洞類似,比如說uaf 釋放後重引用,那麼windows記憶體洩露則屬於分配後未釋放或者未使用,造成的危害雖然遠不及通俗的二進位制漏洞,但是在企業角度看,按數十小時級的工作時間單位來說,造成服務中斷,執行效率減慢,這些問題也是普遍存在的,區別在於是否明顯和帶來影響,和傳統的二進位制漏洞比較,發現的週期更長(Fuzz除外),找出問題的成本要更高,卻又是不可忽略的一個問題

下文來自:https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/finding-a-memory-leak

正文:

當程式從分頁池或非分頁池分配記憶體但不釋放記憶體時發生記憶體洩漏。因此,隨著時間的推移,這些有限的記憶體池將耗盡,導致Windows放慢速度。如果記憶體完全耗盡,可能會導致故障。

一. 驗證漏洞是否存在

如果Windows效能隨著時間的推移而降低,並且懷疑可能涉及記憶體洩漏,則本節中介紹的技術可以指示是否存在記憶體洩漏。它不會告訴你洩漏的來源是什麼,也不知道是使用者模式還是核心模式。

從開始選單搜尋 ‘效能監視器’  新增以下計數器:

Memory– > 池非分頁位元組(Pool Nonpaged Bytes)

Memory– > 池分頁位元組(Pool Paged Bytes)

Paging File – > %使用率(%Usage)

將更新時間更改為600秒,以捕獲一段時間內的洩漏圖。您可能還想將資料記錄到檔案以供日後檢查。啟動你認為是造成洩漏的應用程式進行測試,允許應用程式在測試執行這段時間不受干擾, 在這段時間內不要使用目標計算機,洩漏通常很慢,可能需要幾個小時才能檢測到,等待幾個小時後再判斷是否發生洩漏,監視效能監視器計數器。在測試開始後,計數器值將快速變化,並且可能需要一段時間才能使記憶體池值達到穩定狀態。使用者模式記憶體洩漏始終位於可分頁池中,並導致池分頁位元組計數器和頁面檔案使用率計數器隨著時間的推移穩步增加。核心模式記憶體洩漏通常耗盡非分頁池,導致池非分頁位元組計數器增加,儘管可分頁記憶體也可能受到影響。偶爾這些計數器可能會顯示誤報,因為應用程式正在快取資料。

[翻譯]查詢Windows記憶體洩露的幾種方法

[翻譯]查詢Windows記憶體洩露的幾種方法

[翻譯]查詢Windows記憶體洩露的幾種方法

[翻譯]查詢Windows記憶體洩露的幾種方法

二. 查詢由核心模式的驅動或元件引起的記憶體洩露

1)使用poolmon查詢核心模式的記憶體洩露

如果您懷疑存在核心模式的記憶體洩漏,最簡單方法是使用PoolMon來確定哪個pool tag 是與洩漏關聯的

PoolMon(Poolmon.exe)按池標記名稱(pool tag)監視池的記憶體使用情況,此工具包含在Windows驅動程式工具包(WDK)中。有關完整說明,請參閱WDK文件中的PoolMon。

啟用池標記(Windows 2000和Windows XP)

在Windows 2000和Windows XP上,您必須先使用GFlags啟用池標記。Windows除錯工具中包含GFlags。啟動GFlags,選擇“ 系統登錄檔”選項卡,選中“ 啟用池標記”框,然後單擊“ 應用”。您必須重新啟動Windows才能使此設定生效。有關更多詳細資訊,請參閱GFlags。在Windows Server 2003和更高版本的Windows上,池標記始終處於啟用狀態。

使用PoolMon

PoolMon頭顯示總共分頁和非分頁池位元組。這些列顯示每個池標記的池使用情況。顯示每隔幾秒自動更新一次。例如:

Memory: 16224K Avail: 4564K PageFlts: 31 InRam Krnl: 684K P: 680K
Commit: 24140K Limit: 24952K Peak: 24932K Pool N: 744K P: 2180K

## Tag Type Allocs Frees Diff Bytes Per Alloc

CM Paged 1283 ( 0) 1002 ( 0) 281 1377312 ( 0) 4901
Strg Paged 10385 ( 10) 6658 ( 4) 3727 317952 ( 512) 85
Fat Paged 6662 ( 8) 4971 ( 6) 1691 174560 ( 128) 103
MmSt Paged 614 ( 0) 441 ( 0) 173 83456 ( 0) 482

PoolMon具有根據各種標準對輸出進行排序的命令鍵。按下與每個命令關聯的字母,以重新排序資料。每個命令都需要幾秒鐘的工作。排序命令包括:

[翻譯]查詢Windows記憶體洩露的幾種方法

要使用PoolMon實用工具查詢記憶體洩漏,請執行以下過程:

啟動PoolMon。

如果您確定在非分頁池中發生洩漏,請按P一次,如果您確定它在分頁池中發生,請按P兩次。如果你不知道,不要按P鍵,這兩種都包括在內。

按B按最大位元組使用排序顯示。

開始你的測試。採取螢幕截圖並將其複製到記事本。

每半小時生成一個新的螢幕截圖。通過比較螢幕截圖,確定哪些標籤的位元組正在增加。

停止測試並等待幾個小時。在這個時候有多少標籤被釋放了?

通常情況下,應用程式達到穩定執行狀態後,會以大致相同的速率分配記憶體和空閒記憶體,如果分配記憶體比釋放記憶體更快,記憶體的使用將隨著時間的推移而增長。這通常表示存在記憶體洩漏。

處理洩漏

確定哪個池標籤與洩漏相關後,可能會顯示您需要了解的洩漏資訊。如果您需要確定分配例程的哪個特定例項導致洩漏,請參閱使用核心偵錯程式查詢核心模式記憶體洩漏。

2)使用核心偵錯程式來查詢核心模式的記憶體洩露

啟用池標記(Windows 2000和Windows XP)

在Windows 2000和Windows XP上,您必須先使用GFlags啟用池標記。Windows除錯工具中包含GFlags。啟動GFlags,選擇“ 系統登錄檔”選項卡,選中“ 啟用池標記”框,然後單擊“ 應用”。您必須重新啟動Windows才能使此設定生效。在Windows Server 2003和更高版本的Windows上,池標記始終處於啟用狀態。

確定洩漏的標籤

要確定哪個池標籤與洩漏相關聯,通常最簡單的方法是使用PoolMon工具進行此步驟。有關詳細資訊,請參閱上面使用poolmon來查詢核心模式的記憶體洩露

或者,您可以使用核心偵錯程式查詢與大型池分配關聯的標記,請按照以下步驟操作

1.使用.reload(重新載入模組)命令重新載入所有模組。

2.使用!poolused擴充套件。包含標記“4”以便按頁面記憶體使用排序輸出:

kd> !poolused 4 

Sorting by Paged Pool Consumed

Pool Used:

       NonPaged                     Paged     

Tag            Allocs      Used            Allocs            Used 

Abc                 0          0            36405           33930272 

Tron                0          0            552             7863232 

IoN7                0          0            10939           998432 

Gla5                1         128           2222            924352 

Ggb                 0          0             22             828384

3. 確定哪個池標記與最大的記憶體使用相關聯。在這個例子中,使用標籤“Abc”的驅動程式正在使用最多的記憶體 – 幾乎是34 MB。所以記憶體洩漏最有可能在這個驅動程式中。

確定與洩漏相關聯的池標籤後,請按照以下過程找到洩漏本身:

1.使用ed(Enter Values)命令來修改全域性系統變數PoolHitTag的值。每當使用與其值匹配的池標記時,此全域性變數會導致偵錯程式中斷。

2.將PoolHitTag設定為等於您懷疑是記憶體洩漏源的標記。模組名稱“nt”應該被指定為更快的符號解析。標籤值必須以little-endian格式輸入(即向後)。由於池標籤總是四個字元,這個標籤實際上      是Abc空間,而不僅僅是Abc。所以使用下面的命令:

kd> ed nt!poolhittag ' cbA' 

要驗證PoolHitTag的當前值,請使用db(顯示記憶體)命令:

kd> db nt!poolhittag L4 

820f2ba4  41 62 63 20           Abc.

每次使用標籤Abc分配或釋放池時,偵錯程式都會中斷。每次偵錯程式在這些分配或釋放操作之一中斷時,使用kb(Display Stack Backtrace)偵錯程式命令檢視堆疊跟蹤。

使用此過程,您可以確定駐留在記憶體中的程式碼是否使用標記Abc過度分配池。

要清除斷點,請將PoolHitTag設定為零:

kd> ed nt!poolhittag 0 

如果有幾個不同的地方在分配帶有這個標籤的記憶體,並且這些地方在你編寫的應用程式或驅動程式中,你可以改變你的原始碼為每個分配使用唯一的標籤。如果您不能重新編譯程式,但是您想確定程式碼中幾個可能位置中的哪一個導致洩漏,則可以在每個位置取消組合程式碼,並使用偵錯程式編輯駐留在記憶體中的此程式碼,以便每個例項使用不同的(以前未使用的)池標籤。然後讓系統執行幾分鐘或更長時間。經過一段時間後,再次使用偵錯程式分解並使用!poolfind擴充套件來查詢與每個新標籤關聯的所有池分配。

3)使用驅動程式驗證程式查詢核心模式記憶體洩漏

驅動程式驗證程式確定核心模式驅動程式是否在洩漏記憶體。

Driver Verifier的池跟蹤功能監視指定的驅動程式所做的記憶體分配。在解除安裝驅動程式時,Driver Verifier會驗證驅動程式所有分配的記憶體是否已經釋放。如果某些驅動程式分配的記憶體沒有被釋放,則會發出錯誤檢查,並且錯誤檢查的引數會指出問題的性質。

當此功能處於活動狀態時,使用Driver Verifier Manager圖形介面來監視池分配統計資訊。如果核心除錯程式連線到驅動程式,請使用!verifier 0x3副檔名來顯示分配統計資訊。

如果驅動程式使用直接記憶體訪問(DMA),則驅動程式驗證程式的DMA驗證功能也有助於查詢記憶體洩漏。DMA驗證測試了一些常見的DMA例程錯誤,包括釋放常見緩衝區失敗以及可能導致記憶體洩漏的其他錯誤。如果在此選項處於活動狀態時連線了核心偵錯程式,則使用!dma副檔名顯示分配統計資訊。

有關驅動程式驗證程式的資訊,請參閱Windows驅動程式工具包(WDK)文件中的驅動程式驗證程式。

三. 查詢使用者模式記憶體洩漏

1)使用效能監視器來查詢使用者模式的記憶體洩露

如果您懷疑存在使用者模式記憶體洩漏,但不確定哪個程式導致該洩露,則可以使用效能監視器來測量各個程式的記憶體使用情況,啟動效能監視器。新增以下計數器:

[翻譯]查詢Windows記憶體洩露的幾種方法

Process–>Private Bytes (你要檢查的每一個程式)

Process–>Virtual Bytes (你可能需要檢查的每一個程式)

將更新時間更改為600秒,以捕獲一段時間內的洩漏圖象。您也可以將資料記錄到檔案以供日後檢查。

專用位元組(Private Bytes) 計數器是指一個程式已分配,不包括與其他程式共享儲存器的儲存器總量。

虛擬位元組(Virtual Bytes) 計數器指當前程式正在使用的虛擬地址空間的大小

資料檔案中出現的一些記憶體洩漏,因為分配的私有位元組數增加所以其他記憶體洩漏顯示為虛擬地址空間的增加。

2)使用UMDH查詢使用者模式記憶體洩漏

使用者模式轉儲堆(UMDH)應用程式可與作業系統配合使用便於分析特定程式的Windows堆分配。UMDH可用於定位特定程式中的哪個例程正在洩漏記憶體。

UMDH包含在Windows除錯工具中。詳情請參閱 UMDH。

如果您還沒有確定是哪個程式正在洩漏記憶體,請先參閱使用上面使用效能監視器查詢使用者模式記憶體洩漏

UMDH日誌中最重要的資料是堆分配的堆疊跟蹤。要確定某個程式是否在洩漏堆記憶體,請分析這些堆疊跟蹤

在使用UMDH顯示堆疊跟蹤資料之前,您必須使用GFlags來正確配置您的系統。Windows除錯工具中包含GFlags

以下GFlags設定啟用UMDH堆疊跟蹤:

在GFlags圖形介面中,選擇Image File選項卡,輸入程式名稱(包括副檔名),按TAB鍵,選擇Create user mode stack trace database,然後點選Apply

或者使用以下GFlags命令列,其中ImageName是程式名稱(包括副檔名):

gflags /i ImageName +ust

預設情況下,Windows收集的堆疊跟蹤資料量在x86處理器上限制為32 MB,x64處理器上的64 MB。如果您想增加此資料庫的大小,請選擇GFlags圖形介面中的影像檔案選項卡,輸入程式名稱,按TAB鍵,選中堆疊回溯(Megs)核取方塊,在資料庫中輸入一個值(以MB為單位)關聯文字框,然後單擊應用。僅在必要時增加此資料庫,因為它可能會耗盡有限的Windows資源。當您不再需要較大空間時,請將此設定恢復為原始值。如果您在“ 系統登錄檔”選項卡上更改了任何標誌,則必須重新啟動Windows才能使這些更改生效。如果更改了“ 映像檔案”選項卡上的任何標誌,則必須重新啟動該過程以使更改生效。“ 核心標誌”選項卡的更改立即生效,但在下次重新啟動時會丟失。在使用UMDH之前,您必須有權訪問您的應用程式的正確符號。UMDH使用由環境變數_NT_SYMBOL_PATH指定的符號路徑。將此變數設定為等於包含應用程式符號的路徑。如果您還包含Windows符號的路徑,分析可能會更完整。該符號路徑的語法與偵錯程式使用的語法相同; 有關細節,請參閱符號路徑。

例如,如果您的應用程式的符號檔案位於C:\ MySymbols,並且想要將Windows公共符號儲存用於Windows符號,則可以使用以下命令來設定你的符號路徑:
set _NT_SYMBOL_PATH=c:\mysymbols;srv*c:\mycache*https://msdl.microsoft.com/download/symbols 
另外,為了確保準確的結果,您必須禁用BSTR快取。為此,請將OANOCACHE環境變數設定為一(1)在啟動要跟蹤其分配的應用程式之前進行此設定。

如果您需要跟蹤服務分配,則必須將OANOCACHE設定為系統環境變數,然後重新啟動Windows以使此設定生效。

在Windows 2000上,除了將OANOCACHE設定為1之外,還必須安裝Microsoft支援文章139071提供的修補程式。在Windows XP和更高版本的Windows上不需要此修補程式。
做好這些準備之後,可以使用UMDH捕獲有關程式的堆分配的資訊。為此,請按照以下步驟操作:首先確定您要調查的過程的過程ID(PID)

然後使用UMDH分析此程式的堆記憶體分配,並將其儲存到日誌檔案。將-p開關與PID一起使用,-f開關與日誌檔案的名稱一起使用。例如,如果PID是124,並且想要命名日誌檔案Log1.txt,請使用以下命令:
umdh -p:124 -f:log1.txt 
使用記事本或其他程式開啟日誌檔案。該檔案包含每個堆分配的呼叫堆疊,通過該呼叫堆疊分配的數量以及通過該呼叫堆疊消耗的位元組數來觀察程式的記憶體分配

因為你正在尋找一個記憶體洩漏,單個日誌檔案的內容是不夠的。您必須比較不同時間記錄的日誌檔案,以確定哪些分配正在增長

UMDH可以比較兩個不同的日誌檔案,並顯示其各自分配大小的變化。您可以使用大於號(>)將結果重定向到第三個文字檔案。您可能還需要包含-d選項,該選項將位元組和分配計數從十六進位制轉換為十進位制。例如,要比較Log1.txt和Log2.txt,並將比較結果儲存到檔案LogCompare.txt中,請使用以下命令:

umdh log1.txt log2.txt > logcompare.txt 

開啟LogCompare.txt檔案。其內容類似於以下內容:

+ 5320 ( f110 - 9df0) 3a allocs BackTrace00B53 

Total increase == 5320 

對於UMDH日誌檔案中的每個呼叫堆疊(標記為“BackTrace”),在兩個日誌檔案之間進行比較。在本例中,第一個日誌檔案(Log1.txt)記錄了為BackTrace00B53分配的0x9DF0位元組,而第二個日誌檔案記錄了0xF110個位元組,這意味著在捕獲兩個日誌之間分配了額外的0x5320位元組。這些位元組來自BackTrace00B53標識的呼叫堆疊。要確定回溯中的內容,請開啟原始日誌檔案之一(例如Log2.txt)並搜尋“BackTrace00B53”。結果與這些資料相似:

00005320 bytes in 0x14 allocations (@ 0x00000428) by: BackTrace00B53
ntdll!RtlDebugAllocateHeap+0x000000FD
ntdll!RtlAllocateHeapSlowly+0x0000005A
ntdll!RtlAllocateHeap+0x00000808
MyApp!_heap_alloc_base+0x00000069
MyApp!_heap_alloc_dbg+0x000001A2
MyApp!_nh_malloc_dbg+0x00000023
MyApp!_nh_malloc+0x00000016
MyApp!operator new+0x0000000E
MyApp!DisplayMyGraphics+0x0000001E
MyApp!main+0x0000002C
MyApp!mainCRTStartup+0x000000FC
KERNEL32!BaseProcessStart+0x0000003D  

UMDH輸出顯示從呼叫堆疊分配的總位元組數為0x5320(十進位制數21280)。這些位元組是從0x14(十進位制20)分配的0x428(十進位制1064)個位元組開始分配的。呼叫堆疊被賦予一個識別符號“BackTrace00B53”,並顯示這個堆疊中的呼叫。在檢視呼叫堆疊時,您會看到DisplayMyGraphics例程通過new運算子分配記憶體,該運算子呼叫例程malloc,該malloc使用Visual C ++執行時庫從堆中獲取記憶體。確定這些呼叫中的哪一個是最後一個顯式出現在您的原始碼中。在這種情況下,它可能是新的操作符,因為對malloc的呼叫是作為新實現的一部分發生的,而不是作為單獨的分配。因此,DisplayMyGraphics例程中新運算子的這個例項會重複分配未被釋放的記憶體。

翻譯:https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/finding-a-memory-leak

相關文章