Sysinternals ProcDump 4.0 版

为 Sysinternals ProcDump 4.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 来填充所需的上下文结构。 (A word of caution here: the CONTEXT structure has grown in size over the years as additional registers have been added to processors. 如果更高版本的操作系统增加上下文结构的大小,您需要重新编译此代码。)获取的上下文结构和从 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 替换。 The three values are: Process ID, Event ID and JIT Address. 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) 失踪。 For example: How would you see the code that’s executing? 答案是有点出的--箱。 当调试器需要访问非完全转储中缺少的图像区域时,它获取数据图像文件从相反。 它通过使用的模块加载路径、 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

衷心感谢以下 Microsoft 技术专家对本文的审阅: Drew BlissMark Russinovich