Sunday, March 15, 2009

Nt Insider : 常用的Windbg技巧(3)

相關知識:Windows kernel driver,x86 knowledge, Windbg command
原文連結

技巧4::幫助發現記憶體洩漏的中斷點:記憶體標籤(Tag)

    在開發Windows Kernel Driver的時候,最常使用的記憶體配置的函式之一是ExAllocatePoolWithTag。此函式可以指定一個四個字的標籤(Tag),而配置出來的記憶體便會"貼上"這個標籤。在記憶體洩漏(memory leak)的情況,此標籤就成為解決問題的重要工具!
    不過在使用記憶體標籤功能之前,如果系統的不是Windows 2003、Vista或是更新的版本,那我們需要調整一下系統設定。下載Windbg並且安裝完成之後,此時會發現同一包裡面除了Windbg還有另一個小工具:GFlags。這個小工具讓我們可以藉由UI對系統的各項設定進行調整,好處就是不用去死記這些設定對應的Registry。繼續偵錯之前,先讓我們把"Enable Pool Tagging"打開:GFlag
    題外話,我覺得周大師這篇"Live Debugging環境設定"寫的很棒。
    回到本文,現在我們覺得有Memory leak發生,想中斷在ExAllocatePoolWithTag偵錯。除了前一篇所提利用IAT位置來中斷,也可以使用條件中斷,檢查ExAllocatePoolWithTag的第三個參數,如果是我們的標籤就中斷,其餘放行。
    假設標籤是"BaPd",轉成ACSII碼 B = 42、a = 61、P = 50、d = 64。因為Intel-CPU處理資料的時候遵守Little endian(小頭派)的運作原則,所以開頭的資料會放在記憶體的低位(B在記憶體的地址比a少一個位元組,a又比P少一個位元組),但是在Windbg顯示32bits的時候是由高位(也就是d)到低位(最低的是開頭的B)顯示,所以會看到d(64)、P(50)、a(61)、B(42),串起來就是64506142。依照本系列第一篇所提及的條件中斷語法:

   1: bp nt!ExAllocatePoolWithTag ".if(poi(@esp+0xC) == 64506142){da @esp+0xC L4;k}.else{g}"

    這樣就會在每次(呼叫ExAllocatePoolWithTag) + (Tag = "BaPd")的時候,列印出標籤、堆疊的內容並且中斷。但缺點是條件中斷會讓系統效能明顯的降低X10。由於標籤實在是太重要了,Windows提供了另外一種方法:用Windbg以ed(enter value)指令把想要中斷的標籤輸入到nt!poolhittag,如此一來每當屬於該標籤的記憶體被配置,系統會中斷。記得Intel的小頭政策,輸入標籤時要反向輸入CPU才看的懂。
    下面這個例子我們先清掉原來的條件中斷點,然後輸入'dPaB'到nt!poolhittag,待中斷發生時檢查堆疊與標籤內容是否是我們想要的:

   1: kd> bl
   2:  0 e 82734b10     0001 (0001) nt!ExAllocatePoolWithTag ".if(poi(@esp+0xC) == 64506142){da @esp+0xC l4;kvn}.else{g}"
   3: kd> bc 0
   4: kd> ed nt!PoolHitTag 'dPaB'
   5: d> g
   6: Break instruction exception - code 80000003 (first chance)
   7: nt!DbgBreakPoint:
   8: 8269ef0c cc              int     3
   9: kd> kvn
  10:  # ChildEBP RetAddr  Args to Child              
  11: 00 8273ca88 82735401 80809f70 83d816f8 80829ae0 nt!DbgBreakPoint (FPO: [0,0,0])
  12: 01 8273cadc 829a66c6 00000000 00000038 64506142 nt!ExAllocatePoolWithTag+0x8eb
  13: 02 8273cafc 829acc8f 80831cb8 82743100 00000000 nt!BootApplicationPersistentDataInitialize+0x50
  14: 03 8273cca0 8292ab03 8080a648 39332b42 83540c00 nt!InitBootProcessor+0x412
  15: 04 8273cd0c 82733804 827433c0 82743100 8273d000 nt!KiInitializeKernel+0x770
  16: 05 00000000 00000000 00000000 00000000 00000000 nt!KiSystemStartup+0x32c
  17: kd> da 8273cadc+0x10 L4
  18: 8273caec  "BaPd"

技巧5:手動重建堆疊

    分析dump的時候,如果遇到沒有symbol的module,又恰巧它用了Frame pointer omission的最佳化,在函式的開頭跟結尾省去了對ebp的操作,那麼stack的分析很可能就落的沒有結果,譬如下面的這個例子,Windbg分析堆疊最後斷在savrt,無法得知堆疊更早的資訊,對於偵錯幫助有限:

   1: bd74e438 8045163d nt!IopParseDevice+0xa04
   2: bd74e4ac 804a4e8a nt!ObpLookupObjectName+0x4d5
   3: bd74e5bc 80496b85 nt!ObOpenObjectByName+0xc5
   4: bd74e690 be7728c6 nt!IoCreateFile+0x3ec
   5: bd74e6e0 be77382b savrt+0x458c6
   6: bd74e6ec be7752a5 savrt+0x4682b
   7: bd74e768 be744773 savrt+0x482a5
   8: bd74e77c be742e4b savrt+0x17773
   9: bd74e7a4 be745e75 savrt+0x15e4b
  10: 00000000 00000000 savrt+0x18e75

    怎棒?幸好k指令(Display stack backtrace)可以指定參數,讓Windbg重新分析堆疊,進而逼近原始的真實資料。

k = ebp esp eip

    大致流程是這樣的,當發生先前所舉的那個情況,偵錯者便印出堆疊上的所有資訊,然後依照x86堆疊運作規則,以及邏輯推理,判斷"斷掉的"堆疊所記錄的ebp、esp以及eip(我覺得,應該看成ReturnAddress),然後餵給k指令。其實Call stack的整個追蹤就是靠這三個資訊,Windbg也是使用這三個訊息,反推出整個Call Stack的"長相"。Nt Insider:Stacking the deck是一篇很不錯的參考。
    怎麼起頭呢?我們先在堆疊上找一個"可能是正確"的ebp。ebp會有什麼特徵?ebp會存放前一個stack frame的ebp之位置,而ebp+4則是存放了return address。首先注意到的是第21行:

   1: kd> dps bd74e7a4
   2: bd74e7a4  00000000
   3: bd74e7a8  be745e75 savrt+0x18e75
   4: bd74e7ac  e5d590b0
   5: bd74e7b0  e369d7a8
   6: bd74e7b4  be74355a savrt+0x1655a
   7: bd74e7b8  bd74e7c8
   8: bd74e7bc  e369d7a8
   9: bd74e7c0  e369d7a8
  10: bd74e7c4  be741b0f savrt+0x14b0f
  11: bd74e7c8  00000000
  12: bd74e7cc  e5da1e68
  13: bd74e7d0  e5da1e70
  14: bd74e7d4  be741c26 savrt+0x14c26
  15: bd74e7d8  00000000
  16: bd74e7dc  e5da1e70
  17: bd74e7e0  e1bffb48
  18: bd74e7e4  be8329bc SYMEVENT+0xd9bc
  19: bd74e7e8  00000000
  20: bd74e7ec  e5da1e70
  21: bd74e7f0  bd74e810
  22: bd74e7f4  be834e74 SYMEVENT+0xfe74
  23: bd74e7f8  e5da1e70
  24: bd74e7fc  e1374c28
  25: bd74e800  e5da1e68
  26: bd74e804  bd74e870
  27: bd74e808  e1374c30
  28: bd74e80c  e1374c28
  29: bd74e810  bd74e824
  30: bd74e814  be835023 SYMEVENT+0x10023
  31: bd74e818  e5da1e68
  32: bd74e81c  bd74e870
  33: bd74e820  e1374c48
  34: bd74e824  bd74e838
  35: bd74e828  be82b07f SYMEVENT+0x607f
  36: bd74e82c  bd74e870
     接著發生的事情就要靠一點強記了。取21行(第一個frame)的下一行之值為eip,取29行(第二個frame)之值作為ebp,ebp之值-8為esp。根據我的假設,原文esp的取法是一個隨意,按照Intel CPU的規則esp<ebp,所以這裡指定ebp-8。
   1: kd> k = bd74e810 bd74e810-8 be834e74
   2: ChildEBP RetAddr  
   3: WARNING: Stack unwind information not available. Following frames may be wrong.
   4: bd74e810 be835023 SYMEVENT+0xfe74
   5: bd74e824 be82b07f SYMEVENT+0x10023
   6: bd74e838 be833d28 SYMEVENT+0x607f
   7: bd74e854 be82c7b9 SYMEVENT+0xed28
   8: bd74e894 bfd7fd2a SYMEVENT+0x77b9
   9: bd74e8fc 8041fb8b <EDIT - removed module name>+0xcd2a
  10: bd74e910 8049c945 nt!IopfCallDriver+0x35
  11: bd74ea98 8045163d nt!IopParseDevice+0xa04
  12: bd74eb0c 804a4e8a nt!ObpLookupObjectName+0x4d5
  13: bd74ec1c 80496b85 nt!ObOpenObjectByName+0xc5
  14: bd74ecf0 80497f27 nt!IoCreateFile+0x3ec
  15: bd74ed30 80465691 nt!NtCreateFile+0x2e
  16: bd74ed30 77f8f9c5 nt!KiSystemService+0xc4
  17: 0006e808 00000000 ntdll!NtCreateFile+0xb
    個人覺得這是最難說明的技巧,一方面是在下的文筆與翻譯不好,二方面是因為需要的相關知識太多,很難言傳。如果覺得難以了解,不妨記得K可以指定三個參數,如果遇到Windbg顯示堆疊不對勁,嘗試餵參數給k指令是一個方法。

2 comments:

Nono Liang said...

Hey Royce 大師

借轉貼到我的 Blog 唷...感激!

之前只有試著指定 ebp 給 k 指令,多半仍無法還原call stack的輪廓,最後總是得在stack裡尋找屬於process address space的return address殘跡,且就算找到也不算是很可靠的資訊.

Royce Lu said...

>.^Y 我的榮幸
歡迎交流喔!!