Sysinternals ProcDump v4.0

為 Sysinternals ProcDump v4.0 撰寫外掛程式

Andrew Richards

下載代碼示例

您已經過了一夜安裝任務關鍵型應用程式的最新升級,一切都已經完美。 然後它會發生 — — 在應用程式掛起時,正如每個人都開始上班。 有時,這樣,您需要減少你的損失、 接受釋放是失敗、 收集有關證據,儘快和然後啟動這一極其重要的回滾計畫。

捕獲應用程式有時像這樣的記憶體傾印是常見的故障排除策略,是否為坑、 崩潰或性能方面的原因。 大多數轉儲捕獲工具非此即彼的態度: 他們給你的一切 (完全轉儲) 或很少 (小型轉儲)。 小型轉儲通常太小了富有成效的調試分析不可能的因為一堆人失蹤。 完全轉儲一直是首選,但他們很少再選項。 不斷增加的記憶體是指全部轉儲可以採取 15、 30 或甚至 60 分鐘。 此外,轉儲檔變得太大不能輕鬆地將它們移動分析,甚至在壓縮時。

去年,微軟 ProcDump 3.0 版推出的 MiniPlus 開關 (-mp) 處理本機應用程式的大小問題。 這將創建轉儲介於小型轉儲和全部轉儲的大小。 MiniPlus 交換機的記憶體將納入決策基於大量的考慮記憶體類型、 記憶體保護、 分配大小、 區域堆疊的大小和內容的啟發式演算法。 根據目標應用程式的佈局,轉儲檔可以比完全轉儲小 50%到 95%。 更重要的是,轉儲是一樣功能豐富的大多數分析任務全部轉儲。 當 MiniPlus 交換機應用於 Microsoft Exchange 2010 資訊存儲運行 48 gb 時,結果是記憶體的 1GB–2GB 轉儲檔 (減少 95%)。

Mark Russinovich,並一直對新的發行版本的 ProcDump,現在可以作出決定列入的記憶體。 微軟 ProcDump v4.0 公開相同的 API MiniPlus 作為外部內部使用基於 DLL 的外掛程式通過-d 開關。

在本文中,我要去剖析如何通過建立一系列的示例應用程式,微軟 ProcDump v4.0 工程擴大彼此,實施更多和更多的 ProcDump 功能。 通過深入 ProcDump 在幕後的工作方式,我來告訴你如何編寫外掛程式與 ProcDump 和基礎 DbgHelp API 進行交互。

代碼下載包含示例應用程式和應用程式崩潰以各種方式 (以便您可以測試您的代碼) 的集合。 MiniDump05 示例有所有作為獨立的應用程式實現的 Api。 MiniDump06 示例實現作為一個外掛程式的微軟 ProcDump v4.0 MiniDump05 示例。

用語

它很容易與困惑的轉儲集合相關聯的所有條款 — —"迷你"用了很多的術語。 有的小型轉儲檔案格式、 迷你和 MiniPlus 轉儲內容和 MiniDumpWriteDump 和 MiniDumpCallback 的功能。

Windows 支援通過 DbgHelp.dll 的小型轉儲檔案格式。 小型轉儲轉儲檔 (*.dmp) 是一個容器,支援的記憶體的部分或全部捕獲使用者模式或核心模式目標中。 檔案格式支援使用"流"來存儲其他中繼資料 (注釋,進程統計資訊,等等)。 該檔案格式的名稱取自支援的最少量的資料捕獲的要求。 DbgHelp API MiniDumpWriteDump 和 MiniDumpCallback 的功能被首碼匹配的檔案格式,他們生產的小型轉儲。

迷你,MiniPlus 和全用來描述不同數額的轉儲檔中的內容。 迷你是最小 (最小),包括進程環境塊 (PEB),執行緒環境塊 (瑞侃),部分堆疊、 載入的模組和資料段。 標記,並創造了 MiniPlus 來描述內容的微軟 ProcDump-mp 捕獲 ; 它包括內容的小型轉儲,加上記憶體試探性地當作重要。 全部轉儲 (procdump.exe-馬) 包括的過程,無論在分頁記憶體到記憶體是否整個虛擬位址空間。

MiniDumpWriteDump 函數

要捕獲一個小型轉儲到一個檔的檔案格式的過程,調用 DbgHelp MiniDumpWriteDump 函數。 函數需要 (與 PROCESS_QUERY_INFORMATION 和 PROCESS_VM_READ 訪問),目標進程的控制碼的 PID 的目標進程、 檔 (使用 FILE_GENERIC_WRITE)、 MINIDUMP_TYPE 標誌的位元遮罩和三個可選參數的控制碼: 異常資訊的結構 (用於包括異常上下文記錄) ; (通常用來在 CommentStreamA/W MINIDUMP_STREAM_TYPE 類型通過轉儲中包括注釋) 使用者流資訊結構 ; 和回檔資訊結構 (用於修改什麼捕獲在調用期間):

BOOL WINAPI MiniDumpWriteDump(
  __in  HANDLE hProcess,
  __in  DWORD ProcessId,
  __in  HANDLE hFile,
  __in  MINIDUMP_TYPE DumpType,
  __in  PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
  __in  PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
  __in  PMINIDUMP_CALLBACK_INFORMATION CallbackParam
);

MiniDump01 示例應用程式 (請參見圖 1) 顯示你怎麼叫 MiniDumpWriteDump 沒有任何可選參數的小型轉儲。 它通過檢查 PID 的命令列參數來啟動,然後調用 OpenProcess 獲得目標的進程控制碼。 然後,它調用 CreateFile 獲得的檔案控制代碼。 (注意 MiniDumpWriteDump 支援 I/O 的任何目標)。該檔具有唯一性和按時間順序排序的 ISO 日期/時間基於檔案名: C:\dumps\minidump_YYYY-MM-DD_HH-MM-SS-MS.dmp。 該目錄是硬編碼,以確保寫入訪問 C:\dumps。 做驗屍調試因為當前資料夾 (例如,System32) 可能不是可寫的時候,這是必需的。

圖 1 MiniDump01.cpp

// MiniDump01.cpp : Capture a hang dump.
//
#include "stdafx.h"
#include <windows.h>
#include <dbghelp.h>
int WriteDump(HANDLE hProcess, DWORD dwProcessId, HANDLE hFile, MINIDUMP_TYPE miniDumpType);
int _tmain(int argc, TCHAR* argv[])
{
  int nResult = -1;
  HANDLE hProcess = INVALID_HANDLE_VALUE;
  DWORD dwProcessId = 0;
  HANDLE hFile = INVALID_HANDLE_VALUE;
  MINIDUMP_TYPE miniDumpType;
  // DbgHelp v5.2
  miniDumpType = (MINIDUMP_TYPE) (MiniDumpNormal | MiniDumpWithProcessThreadData |
    MiniDumpWithDataSegs | MiniDumpWithHandleData);
  // DbgHelp v6.3 - Passing unsupported flags to a lower version of DbgHelp
     does not cause any issues
  miniDumpType = (MINIDUMP_TYPE) (miniDumpType | MiniDumpWithFullMemoryInfo |
    MiniDumpWithThreadInfo);
  if ((argc == 2) && (_stscanf_s(argv[1], _T("%ld"), &dwProcessId) == 1))
  {
    // Generate the filename (ISO format)
    SYSTEMTIME systemTime;
    GetLocalTime(&systemTime);
    TCHAR szFilename[64];
    _stprintf_s(szFilename, 64, _T("c:\\dumps\\minidump_%04d-%02d-
      %02d_%02d-%02d-%02d-%03d.dmp"),
        systemTime.wYear, systemTime.wMonth, systemTime.wDay,
        systemTime.wHour, systemTime.wMinute, systemTime.wSecond,
        systemTime.wMilliseconds);
    // Create the folder and file
    CreateDirectory(_T("c:\\dumps"), NULL);
    if ((hFile = CreateFile(szFilename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
      FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE)
    {
      _tprintf(_T("Unable to open '%s' for write (Error: %08x)\n"), szFilename,
        GetLastError());
      nResult = 2;
    }
    // Open the process
    else if ((hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId)) == NULL)
    {
      _tprintf(_T("Unable to open process %ld (Error: %08x)\n"), dwProcessId,
        GetLastError());
      nResult = 3;
    }
    // Take a hang dump
    else
    {
      nResult = WriteDump(hProcess, dwProcessId, hFile, miniDumpType);
    }
    if (hFile) CloseHandle(hFile);
    if (hProcess) CloseHandle(hProcess);
    if (nResult == 0)
    {
      _tprintf(_T("Dump Created - '%s'\n"), szFilename);
    }
    else
    {
      DeleteFile(szFilename);
    }
  }
  else
  {
    _tprintf(_T("Usage: %s <pid>\n"), argv[0]);
    nResult = 1;
  }
  return 0;
}
int WriteDump(HANDLE hProcess, DWORD dwProcessId, HANDLE hFile, MINIDUMP_TYPE miniDumpType)
{
  if (!MiniDumpWriteDump(hProcess, dwProcessId, hFile, miniDumpType, NULL, NULL, NULL))
  {
    _tprintf(_T("Failed to create hang dump (Error: %08x)\n"), GetLastError());
    return 11;
  }
  return 0;
}

DumpType 參數是記憶體的一個基於 MINIDUMP_TYPE 的位元遮罩,導致包括 (或排除) 的特定類型。 MINIDUMP_TYPE 標誌是功能非常強大,並且使您能夠直接捕獲大量的記憶體區域,而無需額外編碼通過回檔。 MiniDump01 示例所使用的選項 ProcDump 所使用的相同。 他們創建 (小型) 轉儲,可以用來總結的過程。

DumpType 總是標誌目前因為它具有的 0x00000000 值 MiniDumpNormal。 使用 DumpType,包括每個堆疊 (MiniDumpNormal)、 所有 PEB 和 TEB 資訊 (MiniDumpWithProcessThreadData)、 載入的模組資訊以及任何全域變數 (MiniDumpWithDataSegs),所有處理資訊 (MiniDumpWithHandleData)、 所有記憶體區域資訊 (MiniDumpWithFullMemoryInfo) 和所有線程的時間與親和資訊 (MiniDumpWithThreadInfo)。 這些標誌,創建轉儲有豐富的小型轉儲,但它的版本仍很小 (小於 30 MB 即使對於最大的應用­陽離子)。 中列出了支援的這些 MINIDUMP_TYPE 標誌的示例調試器命令圖 2

圖 2 調試器命令

MINIDUMP_TYPE 調試器命令
MiniDumpNormal knL99
MiniDumpWithProcessThreadData ! peb,! teb
MiniDumpWithDataSegs lm,dt <global>
MiniDumpWithHandleData ! 處理,! 政務司司長
MiniDumpWithFullMemoryInfo ! 位址
MiniDumpWithThreadInfo ! 失控

時使用 MiniDumpWriteDump,採取轉儲將匹配的架構捕獲程式,並不是目標,因此使用時捕獲一個 32 位的過程,捕獲程式的 32 位版本和 64 位版本的程式捕獲捕捉 64 位進程時。 如果您需要調試"Windows 32 位 Windows 64 位上的"(WOW64),你應該在 32 位進程的 64 位轉儲。

如果你不匹配的體系結構 (意外或故意),你得換來訪問在垃圾場,64 位 32 位堆疊在調試器中的有效機 (.effmach x 86)。 請注意,調試器擴展很多這種情況下失敗。

異常上下文記錄

Microsoft 支援工程師使用術語"坑轉儲"和"故障轉儲"。當他們問崩潰轉儲時,他們想要轉儲的異常上下文記錄。 當他們要求坑轉儲時,他們 (通常情況下) 是指沒有轉儲的一系列。 包含異常資訊轉儲並不總是從崩潰時,時間雖然 ; 它可以是任何時間。 只是一種手段以提供額外資料轉儲異常資訊。 使用者流資訊­資訊是類似于在這方面的異常資訊。

異常上下文記錄是上下文結構 (CPU 寄存器) 和 EXCEPTION_RECORD 結構 (異常代碼、 指令的位址等等) 的組合。 如果您包括異常上下文記錄中的轉儲和運行的.ecxr,調試器當前上下文 (執行緒和註冊狀態) 設置為引發異常的指令 (請參見圖 3)。

上下文切換到異常上下文記錄圖 3

此轉儲檔已存儲在它的利益的異常。

通過.ecxr,可以訪問存儲的異常資訊。

(17cc.1968174c.6f8): Access violation - code c0000005 (first/second chance not available)
eax=00000000 ebx=001df788 ecx=75ba31e7 edx=00000000 esi=00000002 edi=00000000
eip=77c7014d esp=001df738 ebp=001df7d4 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!NtWaitForMultipleObjects+0x15:
77c7014d 83c404          add     esp,4
0:000> .ecxr
eax=00000000 ebx=00000000 ecx=75ba31e7 edx=00000000 esi=00000001 edi=003b3374
eip=003b100d esp=001dfdbc ebp=001dfdfc iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
CrashAV_x86!wmain+0x140xd:
00000001`3f251014 45891b003b100d 8900            mov     dword ptr [r11],r11deax],eax  ds:002b:00000000`00000000=????????=????????

若要支援.ecxr,可選的 MINIDUMP_EXCEPTION_­資訊結構需要傳遞到 MiniDumpWriteDump。 你可以在運行時或異常資訊驗屍。

運行時異常

如果要實現一個調試器事件迴圈,異常資訊傳遞給您,當發生異常。 調試器事件迴圈將接收中斷點、 第一次機會異常和第二次機會異常的 EXCEPTION_DEBUG_EVENT 結構。

MiniDump02 示例應用程式演示如何從 MiniDumpWriteDump 調用調試器事件迴圈內,這樣,第二次機會異常上下文記錄包含在轉儲 (相當於"procdump.exe-e")。 此功能在運行時使用的電子開關。 因為代碼是很長,應用程式的偽代碼所示圖 4。 請參閱本文的代碼下載的完整的原始程式碼。

圖 4 MiniDump02 偽代碼

Function Main
Begin
  Check Command Line Arguments
  CreateFile(c:\dumps\minidump_YYYY-MM-DD_HH-MM-SS-MS.dmp)
  OpenProcess(PID)
  If "–e" Then
    DebugEventDump
    TerminateProcess(Process)
  Else
    WriteDump(NULL)
  CloseHandle(Process)
  CloseHandle(File)
End
Function WriteDump(Optional Exception Context Record)
Begin
  MiniDumpWriteDump(Optional Exception Context Record)
End
Function DebugEventDump
Begin
  DebugActiveProcess(PID)
  While (Not Done)
  Begin
    WaitForDebugEvent
    Switch (Debug Event Code)
    Begin
    Case EXCEPTION_DEBUG_EVENT
      If EXCEPTION_BREAKPOINT
        ContinueDebugEvent(DBG_CONTINUE)
      Else If "First Chance Exception"
        ContinueDebugEvent(DBG_EXCEPTION_NOT_HANDLED)
      Else "Second Chance Exception"
        OpenThread(Debug Event Thread ID)
        GetThreadContext
        WriteDump(Exception Context Record)
        CloseHandle(Thread)
        Done = True
    Case EXIT_PROCESS_DEBUG_EVENT
      ContinueDebugEvent(DBG_CONTINUE)
      Done = True
    Case CREATE_PROCESS_DEBUG_EVENT
      CloseHandle(CreateProcessInfo.hFile)
      ContinueDebugEvent(DBG_CONTINUE)
    Case LOAD_DLL_DEBUG_EVENT
      CloseHandle(LoadDll.hFile)
      ContinueDebugEvent(DBG_CONTINUE)
    Default
      ContinueDebugEvent(DBG_CONTINUE)
    End Switch
  End While
  DebugActiveProcessStop(PID)
End

在應用程式啟動檢查 PID 的命令列參數。 下一步,它調用 OpenProcess 獲得的目標,進程控制碼,然後它調用 CreateFile 獲得的檔案控制代碼。 -E 開關如果缺少它作為前的坑轉儲。 如果存在-電子開關,則應用程式將附加到使用 DebugActiveProcess (作為一個調試器) 目標。 在一段時間迴圈,它等待要經由 WaitForDebugEvent DEBUG_EVENT 結構。 Switch 語句使用的 DEBUG_EVENT 結構的 dwDebugEventCode 成員。 已經被轉儲,或過程結束後,DebugActiveProcessStop 被稱為目標與分離。

EXCEPTION_DEBUG_EVENT 在 DEBUG_EVENT 結構中的包含異常記錄中的異常。 如果異常記錄中斷點,它被處理本地通過調用 ContinueDebugEvent 與 DBG_CONTINUE。 例外情況是第一次的機會,如果它不被處理,使它能變成第二次機會異常 (如果目標沒有處理常式)。 為此,ContinueDebugEvent 被調用 DBG_EXCEPTION_NOT_HANDLED。 其餘的方案是第二次機會異常。 使用的 DEBUG_EVENT 結構的 dwThreadId,OpenThread 被調用以獲取具有異常的執行緒的控制碼。 執行緒控制碼用 GetThreadContext 來填充所需的上下文結構。 (這裡的警告: 上下文結構有所加劇的大小多年來,處理器中添加了額外的寄存器。 如果更高版本的作業系統增加上下文結構的大小,您需要重新編譯此代碼。)獲取的上下文結構和從 DEBUG_EVENT EXCEPTION_RECORD 用於填充 EXCEPTION_POINTERS 結構,,這用來填充一個 MINIDUMP_EXCEPTION_INFORMATION 結構。 與 MiniDumpWriteDump 一起使用的情況下,這種結構傳遞給應用程式的 WriteDump 函數。

EXIT_PROCESS_DEBUG_EVENT 是專門為該方案處理的目標之前發生異常的結束位置。 調用 ContinueDebugEvent 時,DBG_CONTINUE 承認此事件和 while 迴圈並退出。

CREATE_PROCESS_DEBUG_EVENT 和 LOAD_DLL_DEBUG_EVENT 的事件是專門處理控制碼需要被關閉。 這些地區致電 ContinueDebugEvent 與 DBG_CONTINUE。

預設情況下通過調用繼續處理所有其他事件­DebugEvent DBG_CONTINUE 繼續執行,並關閉該控制碼的傳遞。

開機自檢屍體解剖異常

Windows Vista 推出郵政屍調試器命令列支援通過異常資訊的第三個參數。 若要接收第三個參數,您需要有一個調試器值 (以 AeDebug 的關鍵),其中包括三 %ld 替換。 三個值是: 進程 ID、 事件 ID 和 JIT 位址。 JIT 位址是 JIT_DEBUG_INFO 結構的目標位址空間中的位址。 疫情週報 》 未處理的異常的結果被調用時,,Windows 錯誤報告 (週報) 分配此目標的位址空間中的記憶體。 它在 JIT_DEBUG_INFO 結構中填充、 調用後屍體解剖調試器 (分配的位址傳遞),和郵政屍偵錯工具結束後,然後釋放記憶體。

若要確定異常上下文記錄,驗屍應用的 JIT_DEBUG_INFO 結構從讀取目標的位址空間。 結構具有目標的位址空間中的上下文結構與 EXCEPTION_RECORD 結構的位址。 從目的電腦的位址空間中閱讀的上下文和 EXCEPTION_RECORD 的結構,而不是我只填寫這些位址的 EXCEPTION_POINTERS 結構,然後將 ClientPointers 成員設置為 TRUE MINIDUMP_EXCEPTION_INFORMATION 結構中。 這使調試器執行所有這些繁重的任務。 從目的電腦的位址空間 (使津貼體系結構的不同,這樣就可能的 32 位進程轉儲 64 位),它將讀取資料。

MiniDump03 示例應用程式演示如何實現 JIT_DEBUG_INFO 支援 (請參閱圖 5)。

圖 5 MiniDump03 — — JIT_DEBUG_INFO 處理常式

int JitInfoDump(HANDLE hProcess, DWORD dwProcessId, HANDLE hFile, MINIDUMP_TYPE miniDumpType, ULONG64 ulJitInfoAddr)
{
  int nResult = -1;
  JIT_DEBUG_INFO jitInfoTarget;
  SIZE_T numberOfBytesRead;
  if (ReadProcessMemory(hProcess, (void*)ulJitInfoAddr, &jitInfoTarget, sizeof(jitInfoTarget), &numberOfBytesRead) &&
    (numberOfBytesRead == sizeof(jitInfoTarget)))
  {
    EXCEPTION_POINTERS exceptionPointers = {0};
    exceptionPointers.ContextRecord = (PCONTEXT)jitInfoTarget.lpContextRecord;
    exceptionPointers.ExceptionRecord = (PEXCEPTION_RECORD)jitInfoTarget.lpExceptionRecord;
    MINIDUMP_EXCEPTION_INFORMATION    exceptionInfo = {0};
    exceptionInfo.ThreadId = jitInfoTarget.dwThreadID;
    exceptionInfo.ExceptionPointers = &exceptionPointers;
    exceptionInfo.ClientPointers = TRUE;
    nResult = WriteDump(hProcess, dwProcessId, hFile, miniDumpType, &exceptionInfo);
  }
  else
  {
    nResult = WriteDump(hProcess, dwProcessId, hFile, miniDumpType, NULL);
  }
  return nResult;
}

當應用程式調用作為郵政屍調試器時,它是強行終止通過 TerminateProcess 調用進程的應用程式的責任。 在 MiniDump03 的示例中,將轉儲已之後,將調用 TerminateProcess:

// Post Mortem (AeDebug) dump - JIT_DEBUG_INFO - Vista+
else if ((argc == 4) && (_stscanf_s(argv[3], _T("%ld"), &ulJitInfoAddr) == 1))
{
  nResult = JitInfoDump(hProcess, dwProcessId, hFile, miniDumpType, ulJitInfoAddr);
  // Terminate the process
  TerminateProcess(hProcess, -1);
}

開機自檢屍調試器替換為您自己的應用程式,你點 AeDebug 鍵的調試器價值要驗屍應用與合適的體系結構。 通過使用匹配的應用程式,您可以刪除需要對有效的機器 (.effmach) 在調試器中的調整。

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug
Debugger (REG_SZ) = "C:\dumps\minidump03_x64.exe %ld %ld %ld"
HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug
Debugger (REG_SZ) = "C:\dumps\minidump03_x86.exe %ld %ld %ld"

MiniDumpCallback 函數

到目前為止,採取轉儲包含記憶體 DumpType 參數表示要包括 (和 (可選) 異常上下文記錄)。 通過實施 MiniDumpCallback 函數原型,我們可以添加不僅額外的記憶體區域,而且也是一些錯誤處理。 我將描述以後如何,您可以實現完全用微軟 ProcDump v4.0 MiniDumpCallback 函數原型。

有目前 16 回檔類型使您可以控制的傾倒,如記憶體模組和執行緒、 記憶體本身、 取消在進步,轉儲檔 I/O 控制和錯誤處理的轉儲的能力包括多個方面。

Switch 語句我的範本代碼中 (請參閱圖 6) 在他們第一次的 invo 的調用順序大致包括所有回檔類型­陽離子。 一些回檔調用一次以上,可以隨後發生的順序不正確。 同樣,沒有合同訂單,因此它可以在將來的版本更改。

圖 6 範本執行的 MiniDumpCallback 函數原型

BOOL CALLBACK MiniDumpCallbackRoutine(
  __in     PVOID CallbackParam,
  __in     const PMINIDUMP_CALLBACK_INPUT CallbackInput,
  __inout  PMINIDUMP_CALLBACK_OUTPUT CallbackOutput
)
{    // Callback supported in Windows 2003 SP1 unless indicated
  // Switch statement is in call order
  switch (CallbackInput->CallbackType)
  {
  case IoStartCallback:  //  Available in Vista/Win2008
    break;
  case SecondaryFlagsCallback:  //  Available in Vista/Win2008
    break;
  case CancelCallback:
    break;
  case IncludeThreadCallback:
    break;
  case IncludeModuleCallback:
    break;
  case ModuleCallback:
    break;
  case ThreadCallback:
    break;
  case ThreadExCallback:
    break;
  case MemoryCallback:
    break;
  case RemoveMemoryCallback:
    break;
  case WriteKernelMinidumpCallback:
    break;
  case KernelMinidumpStatusCallback:
    break;
  case IncludeVmRegionCallback:  //  Available in Vista/Win2008
    break;
  case IoWriteAllCallback:  //  Available in Vista/Win2008
    break;
  case IoFinishCallback:  //  Available in Vista/Win2008
    break;
  case ReadMemoryFailureCallback:  // Available in Vista/Win2008
    break;
  }
  return TRUE;
}

MiniDump04 示例包含一個回檔,做兩件事 ; 它包括整個堆疊內容,並且忽略的讀取的故障。 此示例使用包括整個堆疊,並忽略讀取的失敗 ReadMemoryFailureCallback ThreadCallback 和 MemoryCallback。

要調用的回檔,可選的 MINIDUMP_CALLBACK_­傳遞給 MiniDumpWriteDump 函數的資訊結構。 結構的 CallbackRoutine 成員用來指向 MiniDumpCallback 功能的實現 (我的範本和示例中為 MiniDumpCallbackRoutine)。 CallbackParam 成員是 VOID * 指標,它允許您保留回檔調用之間的上下文。 我從最小的 WriteDump 函數­Dump04 示例是在圖 7

圖 7 WriteDump 從 MiniDump04 的例子

int WriteDump(HANDLE hProcess, DWORD dwProcessId, HANDLE hFile, MINIDUMP_TYPE miniDumpType, PMINIDUMP_EXCEPTION_INFORMATION pExceptionParam)
{
  MemoryInfoNode* pRootMemoryInfoNode = NULL;
  MINIDUMP_CALLBACK_INFORMATION callbackInfo;
  callbackInfo.CallbackParam = &pRootMemoryInfoNode;
  callbackInfo.CallbackRoutine = MiniDumpCallbackRoutine;
  if (!MiniDumpWriteDump(hProcess, dwProcessId, hFile, miniDumpType, pExceptionParam, NULL, &callbackInfo))
  {
    _tprintf(_T("Failed to create hang dump (Error: %08x)\n"), GetLastError());
    while (pRootMemoryInfoNode)
    {    // If there was an error, we'll need to cleanup here
      MemoryInfoNode* pNode = pRootMemoryInfoNode;
      pRootMemoryInfoNode = pNode->pNext;
      delete pNode;
    }
    return 11;
  }
  return 0;
}

我已定義 (MemoryInfoNode) 保留上下文調用之間的結構是連結的清單節點,其中包含的位址和大小,如下所示:

struct MemoryInfoNode
{
  MemoryInfoNode* pNext;
  ULONG64 ulBase;
  ULONG ulSize;
};

整個堆疊

在 DumpType 參數中使用 MiniDumpWithProcessThreadData 標記時,每個堆疊內容包括從堆疊基地到當前堆疊指標。 我在 MiniDump04 示例中實現的 MiniDumpCallbackRoutine 功能補充這由包括堆疊的其餘部分。 通過包括整個堆疊,您可能能夠確定的堆疊垃圾來源。

基於堆疊的緩衝區會出現緩衝區溢位時,堆疊的垃圾。 緩衝區溢位堆疊的返回位址上寫道,並導致執行的"正義"的操作碼,作為指令指標,而不是按下的"電話"的操作碼指令指標流行緩衝區中的內容。 這將導致在執行無效的記憶體位址,或者甚至更糟、 一塊隨機的代碼的執行。

堆疊垃圾出現時,下麵的記憶體 (請記住,堆疊增長向下) 當前堆疊指標仍將包含未經修改的堆疊的資料。 使用此資料,您可以確定緩衝區的內容和 — — 大部分時間 — — 來生成緩衝區的內容被調用的函數。

如果你比較中採取與無額外的堆疊記憶體傾印的堆疊限制以上記憶體,您將看到顯示記憶體為缺少 (? 符號) 在正常轉儲,但包含在轉儲用回檔中 (請參閱圖 8)。

圖 8 堆疊內容比較

0:003> !teb
TEB at 000007fffffd8000
  ExceptionList:        0000000000000000
  StackBase:            000000001b4b0000
  StackLimit:           000000001b4af000
  SubSystemTib:         0000000000000000
...
// No Callback
0:003> dp poi($teb+10) L6
00000000`1b4af000  ????????`????????
????????`????????
00000000`1b4af010  ????????`????????
????????`????????
00000000`1b4af020  ????????`????????
????????`????????
// Callback
0:003> dp poi($teb+10) L6
00000000`1b4af000  00000000`00000000 00000000`00000000
00000000`1b4af010  00000000`00000000 00000000`00000000
00000000`1b4af020  00000000`00000000 00000000`00000000

"整個堆疊"代碼的第一部分是處理 ThreadCallback 回檔類型 (請參見圖 9)。 此回檔類型是每個進程中的執行緒調用一次。 回檔是通過 CallbackInput 參數傳遞的 MINIDUMP_THREAD_CALLBACK 結構。 結構包括 StackBase 成員是堆疊執行緒的基地。 StackEnd 的成員是當前堆疊指標 (esp/懸浮粒子的 x86 / x64 分別)。 結構不包括堆疊限制 (執行緒環境塊的一部分)。

圖 9 ThreadCallback 用來收集每個執行緒的堆疊區域

case ThreadCallback:
{    // We aren't passed the StackLimit so we use a 1MB offset from StackBase
  MemoryInfoNode** ppRoot = (MemoryInfoNode**)CallbackParam;
  if (ppRoot)
  {
    MemoryInfoNode* pNode = new MemoryInfoNode;
    pNode->ulBase = CallbackInput->Thread.StackBase - 0x100000;
    pNode->ulSize = 0x100000; // 1MB
    pNode->pNext = *ppRoot;
    *ppRoot = pNode;
  }
}
break;

該示例採用了一種簡單的方式,並假定堆疊大小為 1 MB — — 這是大多數應用程式的預設值。 這是堆疊指標下麵的情況下,在 DumpType 參數將導致包括記憶體。 在堆疊大於 1 MB 的情況下,部分的堆疊將會包括。 堆疊小於 1 MB,則附加資料只會包括在內。 請注意是否請求的回檔的記憶體範圍跨越一個免費的區域或與另一個包含重疊,不會發生錯誤。

StackBase 和 1 MB 的偏移量都記錄在我已經定義的 MemoryInfoNode 結構的新實例。 新實例添加到前面的通過 CallbackParam 參數傳遞給回檔的連結清單。 之後的 ThreadCallback 的多個調用,連結清單中包含多個節點,以包括更多的記憶體。

"整個堆疊"代碼的最後一部分是處理 MemoryCallback 回檔類型 (請參見圖 10)。 MemoryCallback 不斷時返回 TRUE 從調用回檔,並為 MemoryBase 和 MemorySize 的 MINIDUMP_CALLBACK_OUTPUT 結構的成員提供一個非零值。

圖 10 MemoryCallback 不斷稱為同時返回一個堆疊區域

case MemoryCallback:
{    // Remove the root node and return its members in the callback
  MemoryInfoNode** ppRoot = (MemoryInfoNode**)CallbackParam;
  if (ppRoot && *ppRoot)
  {
    MemoryInfoNode* pNode = *ppRoot;
    *ppRoot = pNode->pNext;
    CallbackOutput->MemoryBase = pNode->ulBase;
    CallbackOutput->MemorySize = pNode->ulSize;
    delete pNode;
  }
}
break;

代碼 CallbackOutput 參數設置的值,然後從連結清單中刪除該節點。 後的 MemoryCallback 的多個調用,連結清單中將包含沒有更多的節點,並結束 MemoryCallback 調用返回零值。 請注意,MemoryBase 和 MemorySize 的成員被設置為零,當通過 ; 你只需要返回 TRUE。

您可以使用.dumpdebug 命令輸出的 MemoryListStream 區,看到的所有記憶體傾印 (請注意可能合併相鄰塊) 在區域。 請參閱圖 11

圖 11.dumpdebug 命令輸出

0:000> .dumpdebug
----- User Mini Dump Analysis
...
Stream 3: type MemoryListStream (5), size 00000194, RVA 00000E86
  25 memory ranges
  range#    RVA      Address      Size
       0 0000101A    7efdb000   00005000
       1 0000601A    001d6000   00009734
       2 0000F74E    00010000   00021000
       3 0003074E    003b0f8d   00000100
       4 0003084E    003b3000   00001000
...
Read Memory Failure

代碼的最後一個方面是很簡單 (請參見圖 12)。 它將 MINIDUMP_CALLBACK_OUTPUT 結構的狀態成員設置為 S_OK 發出信號它是確定以忽略在捕獲過程中不能讀取的記憶體區域。

圖 12 ReadMemoryFailureCallback 被稱為讀取失敗

case ReadMemoryFailureCallback:  // DbgHelp.dll v6.5; Available in Vista/Win2008
  {    //  There has been a failure to read memory.
Set Status to S_OK to ignore it.
CallbackOutput->Status = S_OK;
  }
  break;

在這個簡單的實現,回檔實現 MiniDumpIgnoreInaccessibleMemory 標誌相同的功能。 ReadMemoryFailureCallback 回檔被通過在 CallbackInput 參數中的 MINIDUMP_READ_MEMORY_FAILURE_CALLBACK 結構的偏移量、 位元組數和故障代碼。 在更複雜的回檔中,可以使用此資訊確定如果記憶體傾印分析、 關鍵和是否應中止轉儲。

夾層的記憶體

你怎麼知道什麼你可以,不能刪除轉儲? 微軟 VMMap 是看到應用程式的記憶體是什麼樣子的好方法。 如果您使用微軟 VMMap 託管進程上,可以看到有分配關聯的垃圾回收 (GC) 堆,和分配關聯應用程式的影像對應檔。 它是在 GC 堆所需的託管進程轉儲中因為兒子的罷工 (SOS) 調試器擴展需要從在 GC 堆轉儲的解釋中的完整的資料結構。

要確定在 GC 堆的位置,您可以通過啟動調試引擎 (DbgEng) 會議,對使用 DebugCreate 和 IDebugClient::AttachProcess 的目標嚴格的方法。 與此調試會話,您可以載入 SOS 調試器擴展,並運行命令返回的堆資訊 (這是使用領域知識的示例)。

或者,您可以使用啟發式掃描。 您包含有私人 (MEMORY_PRIVATE) 或保護的讀寫記憶體類型的所有地區 (PAGE_READWRITE 或 PAGE_EXECUTE_­讀寫)。 這會收集更多的記憶體,除非絕對必要,但仍然是一項重大節省通過排除應用程式本身。 MiniDump05 示例採用這種方法 (請參閱圖 13) MiniDump04 示例的執行緒堆疊代碼替換 (新的邏輯仍然會導致整個堆疊將會作為前) ThreadCallback 回檔是一次性一定迴圈。 然後,使用相同的 MemoryCallback 代碼 MiniDump04 示例中用於包含在轉儲中的記憶體。

圖 13 MiniDump05 — ThreadCallback 一次用於收集記憶體區域

case ThreadCallback:
{    // Collect all of the committed MEM_PRIVATE and R/W memory
  MemoryInfoNode** ppRoot = (MemoryInfoNode**)CallbackParam;
  if (ppRoot && !*ppRoot)    // Only do this once
  {
    MEMORY_BASIC_INFORMATION mbi;
    ULONG64 ulAddress = 0;
    SIZE_T dwSize = VirtualQueryEx(CallbackInput->ProcessHandle, (void*)ulAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION));
    while (dwSize == sizeof(MEMORY_BASIC_INFORMATION))
    {
      if ((mbi.State == MEM_COMMIT) &&
        ((mbi.Type == MEM_PRIVATE) || (mbi.Protect == PAGE_READWRITE) || (mbi.Protect == PAGE_EXECUTE_READWRITE)))
      {
        MemoryInfoNode* pNode = new MemoryInfoNode;
        pNode->ulBase = (ULONG64)mbi.BaseAddress;
        pNode->ulSize = (ULONG)mbi.RegionSize;
        pNode->pNext = *ppRoot;
        *ppRoot = pNode;
      }
      // Loop
      ulAddress = (ULONG64)mbi.BaseAddress + mbi.RegionSize;
      dwSize = VirtualQueryEx(CallbackInput->ProcessHandle, (void*)ulAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION));
    }
  }
}
break;

記憶體映射的影像檔

您可能想知道如何可以調試轉儲的圖像區域 (MEM_IMAGE) 失蹤。 例如: 您怎麼會看到執行的代碼? 答案是有點出的--箱。 當調試器需要訪問非完全轉儲中缺少的圖像區域時,它獲取資料影像檔從相反。 它通過使用的模組載入路徑、 PDB 的原始影像檔的位置或使用.sympath/.exepath 搜索路徑查找影像檔。 如果在運行 lmvm <module>,您將看到一個"映射記憶體影像檔"線,表明該檔已在映射到轉儲,如下所示:

0:000> lmvm CrashAV_x86
start    end        module name
003b0000 003b6000   CrashAV_x86   (deferred)            
  Mapped memory image file: C:\dumps\CrashAV_x86.exe
  Image path: C:\dumps\CrashAV_x86.exe
  Image name: CrashAV_x86.exe
...

依靠調試器的"映射記憶體影像檔"功能是保持轉儲的大小更小的好方法。 它工作特別好與本機應用程式,因為編譯的二進位檔案是使用的因此您的內部生成伺服器上可用 (並指出通過 Pdb)。 託管的應用程式,遠端客戶的電腦上的 JIT 編譯會使這複雜化。 如果要調試託管應用程式的轉儲從另一台電腦,您需要複製二進位檔案 (以及轉儲) 本地。 因為可以迅速,採取多個轉儲,然後單 (大) 應用影像檔集合可以在不停機的情況下,這仍然是儲蓄。 為了簡化檔集合,您可以使用 ModuleCallback 寫出一個腳本,收集在轉儲中引用的模組 (檔)。

插我 !

V4.0 改變獨立應用程式使用微軟 ProcDump 讓你的生活要容易得多。 您不再需要執行的所有代碼關聯與調用 MiniDumpWriteDump,並且,更重要的是,所有的代碼,以在適當的時間觸發轉儲。 您只需執行 MiniDumpCallback 函數並將其匯出為 MiniDumpCallbackRoutine 在 DLL 中。

MiniDump06 示例 (請參閱圖 14) 包括來自 MiniDump05 幾個修改後的回檔代碼。

圖 14 MiniDumpCallbackRoutine 改為使用全球,而不是 CallbackParam

MemoryInfoNode* g_pRootMemoryInfoNode = NULL;
...
case IncludeThreadCallback:
{
  while (g_pRootMemoryInfoNode)
  {    //Unexpected cleanup required
    MemoryInfoNode* pNode = g_pRootMemoryInfoNode;
    g_pRootMemoryInfoNode = pNode->pNext;
    delete pNode;
  }
}
break;
...
case ThreadCallback:
{    // Collect all of committed MEM_PRIVATE and R/W memory
  if (!g_pRootMemoryInfoNode)    // Only do this once
  {
...
pNode->pNext = g_pRootMemoryInfoNode;
    g_pRootMemoryInfoNode = pNode;
...
}
}
break;
...
case MemoryCallback:
{    // Remove the root node and return its members in the callback
  if (g_pRootMemoryInfoNode)
  {
    MemoryInfoNode* pNode = g_pRootMemoryInfoNode;
    g_pRootMemoryInfoNode = pNode->pNext;
    CallbackOutput->MemoryBase = pNode->ulBase;
    CallbackOutput->MemorySize = pNode->ulSize;
    delete pNode;
  }
}
break;

新的 MiniDump06 專案編譯的回檔代碼作為 DLL。 該專案出口使用 DEF 檔 MiniDumpCallbackRoutine (區分大小寫):

LIBRARY    "MiniDump06"
EXPORTS
  MiniDumpCallbackRoutine   @1

ProcDump 通過 CallbackParam NULL 值,因為功能需要使用一個全域變數來跟蹤其進度,通過我的 MemoryInfoNode 結構。 在第一的 IncludeThreadCallback,有新的代碼,以重置 (刪除) 全域變數如果設置了從以前的捕獲。 這將替換已實施後失敗的 MiniDumpWriteDump 我的 WriteDump 函數中調用的代碼。

要與 ProcDump 一起使用 DLL,請指定-d 開關跟著您匹配的捕獲體系結構的 DLL 的名稱。 -D 開關,則可用時以小型轉儲 (沒有開關) 和完整 (-馬) 轉儲 ; MiniPlus 時,它並不是可用 (-mp) 轉儲:

procdump.exe -d MiniDump06_x64.dll notepad.exe
procdump.exe –ma -d MiniDump06_x64.dll notepad.exe

請注意比我的樣本時全部轉儲中所述的不同回檔類型調用回檔 (-馬) 正在採取 (請參閱 MSDN 庫的文檔)。 當 DumpType 參數包含 MiniDumpWithFullMemory,MiniDumpWriteDump 函數會將轉儲視為全部轉儲的情況。

微軟 ProcDump (procdump.exe) 是一個提取物本身的 64 位版本 (procdump64.exe) 時所需的 32 位應用程式。 已提取並推出的 procdump.exe procdump64.exe 後,procdump64.exe 將載入 DLL (64 位)。 因此,調試 64 位 DLL 是相當困難的因為啟動的應用程式不是所需的目標。 支援 64 位 DLL 的調試做最簡單的就是將臨時 procdump64.exe 複製到另一個資料夾,然後使用該副本進行調試。 這種方式,沒有提取將會出現,並且將您從啟動的應用程式中載入 DLL 在調試器中 (例如 Visual Studio)。

打破 !

確定原產地的崩潰和掛起不容易時您可以承受只小型轉儲。 製作與其他關鍵資訊轉儲檔解決了這個不會引起的全部轉儲的開支。

如果你有興趣在執行您自己的轉儲應用程式或 DLL,我建議你先調查微軟 ProcDump、 週報和 AdPlus 實用程式的有效性 — — 不另起爐灶。

當編寫回檔,確保你花時間去瞭解您的應用程式內的記憶體的精確佈局。 微軟 VMMap 快照,並使用該應用程式的轉儲深入研究。 小轉儲是反覆運算的方法。 排除明顯的領域,包括啟動,然後調整你的演算法。 您可能需要使用域和啟發式知識的方法讓你向你的目標。 您可以通過修改目標應用程式來説明您的決策制定。 例如,可以使用知名 (和獨特) 分配大小為每種類型的記憶體使用。 重要的是去創造性的思考時決定如何確定哪些記憶體是必需的在目標應用程式和傾倒申請。

如果您是內核開發人員,並且有興趣在回檔中,有類似的基於內核的機制 ; 請參閱 BugCheckCallback 常式的文檔上 msdn.com

Andrew Richards 是微軟升級高級工程師為 Windows OEM。他熱衷於支援工具,並不斷創造調試器擴展和回檔,和應用程式來簡化的工作支援工程師。他可以致電 andrew.richards@microsoft.com

多虧了以下技術專家,檢討這篇文章: 德魯幸福馬克 Russinovich