简介
在过去几年里,崩溃转储(crash dump)成为了调试工作的一个重要部分。如果软件在客户现场或者测试实验室发生故障,最有价值的解决方式是能够创建一个故障瞬间的应用程序状态镜像,然后可以在开发者的机器上通过调试器进行分析。第一代的crash dump通常被称为“全用户转储(full user dump)”,它包含了进程的虚拟内存的全部内容。毫无疑问,这样的dump对于事后调试非常有价值。但是,这样的dump经常非常大,使得通过电子方式发送给开发者非常困难,甚至没法完成。另外,没用公共接口可以通过程序调用来创建dump,我们必须依赖于第三方工具(例如,Dr. Watson 或者Userdump)来创建他们。
随着Windows XP,微软发布了一组新的被称为“minidump”的崩溃转存技术。Minidump很容易定制。按照最常用的配置,一个minidump只包括了最必要的信息,用于恢复故障进程的所有线程的调用堆栈,以及查看故障时刻局部变量的值。这样的dump文件通常很小(只有几K字节)。所以,很容易通过电子方式发送给软件开发人员。一旦需要,minidump甚至可以包含比原来的crash dump更多的信息。例如,可以包含进程使用的内核对象的信息。另外,DbgHelp.dll提供了通过编程创建minidump的公开API。而且,它是可以重新发布的。我们可以不再依赖于外部工具。
minidump可以定制,给我们带来了一个问题-保存多少应用程序状态信息才能既保证调试有效,又能够尽量保证minidump文件尽可能小?尽管调试简单的异常访问只需要调用堆栈和局部变量的信息,但是解决更复杂的问题需要更多的信息。例如,我们可能需要查看全局变量的值、检查堆的完整性和分析进程虚拟内存的布局。同时,可执行程序的代码段往往是多余的,开发用的机器上可以很容易找到这些执行程序。
幸运的是我们可以通过DbgHelp函数组(MiniDumpWriteDump和MiniDumpCallback)来控制这些功能,甚至可以更复杂。在这篇文章里面,我们会解释怎么样使用这些函数来创建mindump,保证文件足够小但是又能有效调试。也会讲解minidump中应该包括那些数据,并且如何使用通用调试器(WinDbg和VS.NET)来看这些信息。
Minidump类型
先看一些代码。Figure 1是MiniDumpWriteDump的函数声明。Figure 2 显示如何使用这个函数创建简单的minidump。
Figure 1:
BOOL MiniDumpWriteDump(
HANDLE hProcess,
DWORD ProcessId,
HANDLE hFile,
MINIDUMP_TYPE DumpType,
PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
PMINIDUMP_CALLBACK_INFORMATION CallbackParam
);
Figure 2:
void CreateMiniDump( EXCEPTION_POINTERS* pep )
{
// Open the file
HANDLE hFile = CreateFile( _T("MiniDump.dmp"), GENERIC_READ | GENERIC_WRITE,
0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
if( ( hFile != NULL ) && ( hFile != INVALID_HANDLE_VALUE ) )
{
// Create the minidump
MINIDUMP_EXCEPTION_INFORMATION mdei;
mdei.ThreadId = GetCurrentThreadId();
mdei.ExceptionPointers = pep;
mdei.ClientPointers = FALSE;
MINIDUMP_TYPE mdt = MiniDumpNormal;
BOOL rv = MiniDumpWriteDump( GetCurrentProcess(), GetCurrentProcessId(),
hFile, mdt, (pep != 0) ? &mdei : 0, 0, 0 );
if( !rv )
_tprintf( _T("MiniDumpWriteDump failed. Error: %u \n"), GetLastError() );
else
_tprintf( _T("Minidump created.\n") );
// Close the file
CloseHandle( hFile );
}
else
{
_tprintf( _T("CreateFile failed. Error: %u \n"), GetLastError() );
}
}
在这个例子里面,我们如何指定minidump应该包括那些数据呢?主要取决于MiniDumpWriteDump的第四个参数MINIDUMP_TYPE。下表Figure 3是参数的定义。
Figure 3:
typedef enum _MINIDUMP_TYPE {
MiniDumpNormal = 0x00000000,
MiniDumpWithDataSegs = 0x00000001,
MiniDumpWithFullMemory = 0x00000002,
MiniDumpWithHandleData = 0x00000004,
MiniDumpFilterMemory = 0x00000008,
MiniDumpScanMemory = 0x00000010,
MiniDumpWithUnloadedModules = 0x00000020,
MiniDumpWithIndirectlyReferencedMemory = 0x00000040,
MiniDumpFilterModulePaths = 0x00000080,
MiniDumpWithProcessThreadData = 0x00000100,
MiniDumpWithPrivateReadWriteMemory = 0x00000200,
MiniDumpWithoutOptionalData = 0x00000400,
MiniDumpWithFullMemoryInfo = 0x00000800,
MiniDumpWithThreadInfo = 0x00001000,
MiniDumpWithCodeSegs = 0x00002000,
MiniDumpWithoutManagedState = 0x00004000,
} MINIDUMP_TYPE;
MINIDUMP_TYPE枚举是一些标志,允许我们来控制minidump包含哪些内容。我们来看一下这些值得内容,以及如何使用它们。
MiniDumpNormal
MiniDumpNormal是一个特别的标志。它的值是0,意味着这个值永远隐含存在,甚至不需要显示指定。因此,我们可以假定这个标记代表了minidump中永远存在的一组基础数据集合。通过指定用户自定义的回调函数,可以过滤这些值。
Figure 4的表格显示了数据基础数据集合中的数据类型。
Figure 4:
数据类型 |
描述 |
系统信息 |
关于操作系统和CPU的信息,包括:
在WinDbg中,可以通过“vertarget” 和 “!cpuid”显示相应信息。 |
进程信息 |
关于进程(Process)的信息,包括:
WinDbg通过| (Process Status)命令显示进程ID,“.time”显示进程时间。 |
模块(Module) 信息 |
对于进程装载的所有可执行模块,显示如下信息:
在WinDbg和 VS.NET中,可以在Modules窗口中看到这些信息。WinDbg的“lm”也可以看到这些信息。 |
线程信息 |
对于进程中的任何一个线程,会包括这些信息:
VS.NET中,Threads窗口中可以显示大多数这些信息。WinDbg中用 “~”命令显示线程信息。 |
线程栈 |
对于每一个线程,minidump包含了栈内存的内容。允许我们得到所有线程的调用栈,查看函数参数和局部变量的值。 |
指令窗口 |
对于每一线程,当前指令指针前后的256自己内存会保留下来。允许我们即使没有可执行模块,也可以获得故障时刻的线程代码的反编译信息。 |
异常信息 |
可以通过MiniDumpWriteDump 函数的第5个参数(见Figure 2)把异常信息包含到minidump中。这种情况下minidump会包括如下异常信息:
当VS.NET debugger 装载带有异常信息的minidump数据, debugger会自动显示异常时刻应用程序状态(包括调用堆栈、寄存器值、反汇编的指令和抛出异常的代码行)。WinDbg中,需要使用.ecxr命令切换到异常发生时刻的应用程序状态。 |
确实,MiniDumpNormal指定的基础信息集合非常有用。我们可以定位出现问题的指令,检查线程怎么样进入到这种状态。甚至可以产看到函数参数和局部变量的值。另外,这些信息也可以用来调试死锁,因为我们可以看到所有线程的调用栈,并且知道他们在等待什么。
同时,所有这些信息的代价非常小,minidump的大小通常不超过20KB。主要影响大小的因素的线程栈的大小-他们占用的内存越多,minidump的文件越大。
但是,如果需要调试的问题比较复杂,而不是像非法访问或者死锁这样的简单问题,我们就会发现MiniDumpNormal标记收集的信息还不够。我们有可能需要查看全局变量,但是里面没有。也有可能需要查看堆里面分配的结构体的内容,minidump也没有包括相应的堆信息。当我们需要更多的minidump数据时,就需要研究MINIDUMP_TYPE的其他成员了。
MiniDumpWithFullMemory
这可能是除了MiniDumpNormal以外使用最多的标志了。如果指定了这个标志,minidump会包含进程地址空间中所有可读页面的内容。我们可以看到应用程序分配的所有内存,这使我们有很多的调试方法。可以查看存储在栈上、堆上、模块数据段的所有数据。甚至还可以看到线程和进程环境块(Process Environment Block和Thread Environment Bolck, PEB和TEB)的数据。这些没有公开的数据结构可以给我们的调试提供无价的帮助。
使用这个标记的唯一问题是会使minidump变得很大,至少有几MByte。另外,minidump的内容里面包含了冗余信息,所有可执行模块的代码段都包含在了里面。但是很多时候,我们很容易从其他地方获得可执行代码。让我们一起来看看MINIDUMP_TYPE,是否能够找到更好的选项。
MiniDumpWithPrivateReadWriteMemory
如果指定这个标志,minidump会包括所有可读和可写的私有内存页的内容。这使我们可以察看栈、堆甚至TLS的数据。PEB和TEB也包括在里面。
这时候,minidump没有包括共享内存也的内容。也就是说,我们不能查看内存映射文件的内容。同样,可执行模块的代码和数据段也没有包括进来。不包括代码段意味着dump没有占用不需要的空间。但是,我们也没有办法查看全局变量的值。
无论如何,通过组合其他一些选项,MiniDumpWithPrivateReadWriteMemory是一个非常有用的选项。我们会在后面看到。
MiniDumpWithIndirectlyReferencedMemory
如果指定这个标志,MiniDumpWriteDump检查线程栈内存中的每一个指针。这些指针可能指向线程地址空间的其他可读内存页。一旦发现这样的指针,程序会读取指针附近1024字节的内容存到minidump中(指针前的256字节和指针后的768字节)。
Figure 5是一段例子代码.
Figure 5:
#include <stdio.h>
struct A
{
int a;
void Print()
{ printf("a: %d\n", a); }
};
struct B
{
A* pA;
B(): pA(0) {}
};
int main( int argc, char* argv[] )
{
B* pB = new B();
pB->pA->Print();
return 0;
}
在这个例子中,主程序试图通过null对象指针(pB->pA)调用A::Print。这会导致一个运行时非法访问。如果使用MiniDumpNormal产生的minidumo来调试,会发现没有办法看到指针pB指向的结构体的内容。这些内容存在堆上。我们只能猜测传给A::Print的对象指针是null。
如果我们指定了标志MiniDumpWithIndirectlyReferencedMemory,MiniDumpWriteDump会发现栈上有一个指针pB指向了堆上的其他区域。就会把pB指向地址附近的1024字节存到minidump中。因此,通过调试器就可以看到结构体B的内容,进而发现pA是null。
当然,MiniDumpWriteDump不能访问调试信息。因此,他没有办法区分真正的指针和另外一些值。这些值恰好可以被认为指向有效内存区域。Figure 6.解释了这种情况。
Figure 6:
#include <stdio.h>
void PrintSum( unsigned long sum )
{
printf( "sum: %x", sum );
// access violation
*(int*)0 = 1;
}
unsigned long Sum( unsigned long a, unsigned long b )
{
unsigned long sum = a + b;
PrintSum( sum );
return sum;
}
int main()
{
Sum( 0x10000, 0x120 );
return 0;
}
当PrintSum导致非法访问的时候,0x10000和0x120的和保存在栈上。这个和(0x10120)不是指针。但是,MiniDumpWriteDump没有办法知道。如果0x10120恰好是可读内存页的有效地址,minidump会包括1024字节的内存(0x10020 – 0x10520)。
当搜索栈的时候,MiniDumpWriteDump会忽略指向可执行模块的数据段的指针。这就导致MiniDumpWithIndirectlyReferencedMemory没办法让我们看到全局变量的值。即使栈指向它们都不行。后面我们会看到,MINIDUMP_TYPE还包括其他标志可以完成这个功能。
加上MiniDumpWithIndirectlyReferencedMemory标记,minidump大小会增加。增加的数量取决于栈中指针的数量。
MiniDumpWithDataSegs
如果指定这个标志,minidump会包括进程装载的所有可执行模块的可写数据段。如果我们希望查看全局变量的值,有不希望被MiniDumpWithFullMemory困扰,就可以使用MiniDumpWithDataSegs。
这个标志对于minidump大小的影响完全取决于相关数据段的大小。系统DLL的数据段也包含在内,所以,即使一个简单的程序,也可能会增加几百KB。 例如,DbgHelp的.data段超过100K。如果我们只是为了使用MiniDumpWriteDump,这代价可能太大了。在文章的后半部分,会给大家演示,怎么样控制MiniDumpWriteDump来保证只包含真正需要的数据段。
MiniDumpWithCodeSegs
如果指定这个标志,mindump会包括所有进程装载的可执行模块的代码段。就像MiniDumpWithDataSegs,minidump大小会有明显增长。在文章的后半部分,我会演示增么样定制MiniDumpWriteDump,保证只包含必要的代码段。
MiniDumpWithHandleData
如果指定这个标志,minidump会包括故障时刻进程故障表里面的所有句柄。可以用WinDbg的!handle来显示这些信息。
这个标志对于minidump大小的影响取决于进程句柄表中的句柄数量。
MiniDumpWithThreadInfo
MiniDumpWithThreadInfo可以帮助收集进程中线程的附加信息。对于每一个线程,会提供下列信息:
- 线程时间 (创建时间,执行用户代码和内核代码的时间)
- 入口地址
- 相关性
WinDbg中,可以通过.ttime命令查看线程时间。
MiniDumpWithProcessThreadData
有些时候我们需要查看线程和进程环境块的内容(PEB和TEB)。假设minidump包括了这些块占用的内存,就可以通过WinDbg的!peb和!teb命令来查看。这正是MiniDumpWithProcessThreadData所提供的数据。当使用这个标志时,minidump会包含PEB和TEB占据的内存页。同时,也包括了另外一些它们也用的内存页(例如,环境变量和进程参数保存的位置,通过TlsAlloc分配的TLS空间)。遗憾的是,有一些PEB和TEB引用的内存被忽略了,例如,通过__declspec(thread)分配的线程TLS数据。如果确实需要,就不得不使用MiniDumpWithFullMemory或者MiniDumpWithPrivateReadWriteMemory来获得。
MiniDumpWithFullMemoryInfo
如果希望检查整个继承的虚拟内存布局,我们可以使用MiniDumpWithFullMemoryInfo标志。如果指定它,mindump会包含进程虚拟内存布局的完整信息。可以通过WinDbg的!vadump和!vprot命令查看。这个标志对minidump大小的影响取决于虚拟内存布局-每个有相似特性的页面区域(参考VirtualQuery函数说明)会增加48字节。
MiniDumpWithoutOptionalData
我们已经看过的所有MINIDUMP_TYPE标记都是想minidump中添加一些数据。也有一些标志作用相反,它们从minidump中去除一些不必要的数据。MiniDumpWithoutOptionalData就是其中一个。他可以用来减小保存在dump中的内存的内容。当指定这个标志是,只有MiniDumpNormal指定的内存会被保存。其他内存相关的标志(MiniDumpWithFullMemory, MiniDumpWithPrivateReadWriteMemory, MiniDumpWithIndirectlyReferencedMemory)即使指定,也是无效的。同时,他不影响这些标志的行为:MiniDumpWithProcessThreadData, MiniDumpWithThreadInfo, MiniDumpWithHandleData, MiniDumpWithDataSegs, MiniDumpWithCodeSegs, MiniDumpWithFullMemoryInfo
MiniDumpFilterMemory
如果指定这个标志,栈内存的内容会在保存之前进行过滤。只有重建调用栈需要的数据才会被保留。其他数据会被写成0。也就是说,调用栈可以被重建,但是所有局部变量和函数参数的值都是0。
这个标志不影响minidump的大小。它只是没有改变保存的内存数量,只是把其中一部分用0覆盖了。同时,这个标志只影响线程栈占用内存的内容。其他内存(比如堆)不受影响。如果使用了MiniDumpWithFullMemory,这个标志就不起作用了。
MiniDumpFilterModulePaths
这个标志控制模块信息中是否包括模块路径(参考MiniDumpNormal的说明)。如果指定这个标记,模块路径会从dump中删除,只保留模块的名字。按照文档说明,它也可以帮助从minidump中删除可能涉及隐私的信息(例如有些时候模块的路径会包含用户名)。
由于模块路径数量不多,这个标志对minidump的大小影响不大。对调试的影响也不大。我们经常需要告诉调试器匹配的可执行程序保存的位置。
MiniDumpScanMemory
这个标志可以帮助我们节约minidump占用的空间。它会把调试不需要的可执行模块去掉。这个标志会和MiniDumpCallback函数紧密合作。因此,我们首先看一下这个函数,然后回头讨论MiniDumpScanMemory。
MiniDumpCallback函数
如果MINIDUMP_TYPE不能满足我们定制minidump内容的需要,我们可以使用MiniDumpCallback函数。这是一个用户定义的回调函数,MiniDumpWriteDump会调用它,让用户来决定是否把某些数据放到minidump中。通过这个函数,我们可以完成这些功能:
- 从minidump的模块信息中移除一个可执行模块信息(部分或者全部)
- 从minidump的线程信息中移除一个线程信息(部分或者全部)
- 在minidump中添加一段用户指定范围的内存的内容
让我们先看一下MiniDumpCallback 的声明(见Figure 7):
Figure 7:
BOOL CALLBACK MiniDumpCallback(
PVOID CallbackParam,
const PMINIDUMP_CALLBACK_INPUT CallbackInput,
PMINIDUMP_CALLBACK_OUTPUT CallbackOutput
);
这个函数有四个参数。第一个参数CallbackParam是一个用户为回调函数定义的数据结构(例如,一个指向C++对象的指针)。第二个参数CallbackInput是MiniDumpWriteDump传递给回调函数的数据。第三个参数CallbackOutput包含了回调函数返回给MiniDumpWriteDump的数据。这个数据通常就是指定关于那些数据应该包含在minidump中。
现在,让我们看一下MINIDUMP_CALLBACK_INPUT和MINIDUMP_CALLBACK_OUTPUT结构体的内容。
Figure 8:
typedef struct _MINIDUMP_CALLBACK_INPUT {
ULONG ProcessId;
HANDLE ProcessHandle;
ULONG CallbackType;
union {
HRESULT Status;
MINIDUMP_THREAD_CALLBACK Thread;
MINIDUMP_THREAD_EX_CALLBACK ThreadEx;
MINIDUMP_MODULE_CALLBACK Module;
MINIDUMP_INCLUDE_THREAD_CALLBACK IncludeThread;
MINIDUMP_INCLUDE_MODULE_CALLBACK IncludeModule;
};
} MINIDUMP_CALLBACK_INPUT, *PMINIDUMP_CALLBACK_INPUT;
typedef struct _MINIDUMP_CALLBACK_OUTPUT {
union {
ULONG ModuleWriteFlags;
ULONG ThreadWriteFlags;
struct {
ULONG64 MemoryBase;
ULONG MemorySize;
};
struct {
BOOL CheckCancel;
BOOL Cancel;
};
HANDLE Handle;
};
} MINIDUMP_CALLBACK_OUTPUT, *PMINIDUMP_CALLBACK_OUTPUT;
typedef enum _MINIDUMP_CALLBACK_TYPE {
ModuleCallback,
ThreadCallback,
ThreadExCallback,
IncludeThreadCallback,
IncludeModuleCallback,
MemoryCallback,
CancelCallback,
WriteKernelMinidumpCallback,
KernelMinidumpStatusCallback,
} MINIDUMP_CALLBACK_TYPE;
MINIDUMP_CALLBACK_INPUT结构体包含MiniDumpWriteDump对回调函数的请求。前两个成员意义很明显-创建minidump的进程的id和句柄。第三个成员CallbackType是请求的类型,通常叫做回调类型。所有CallbackType的可能的值定义在MINIDUMP_CALLBACK_TYPE枚举集合中(见Figure 8)。我们在后面会仔细看一下这些值。结构体的第四个参数是一个联合,它的意义依赖于CallbackType的值。这个联合包含了MiniDumpWriteDump请求的附加数据。
MINIDUMP_CALLBACK_OUTPUT结构体要简单一点。它有一个联合构成,联合的意义依赖于MINIDUMP_CALLBACK_INPUT的值。联合的CallbackType成员包含了回调对于MiniDumpWriteDump的反馈。
下面我们来过一下回调类型(callback type)对应的一些最终重要的请求,以及回调函数如何对他们做出响应。在开始之前,先看一下Figure 9。这个例子表示了怎么样告诉MiniDumpWriteDump有一个用户自定的回调函数需要调用。
Figure 9:
void CreateMiniDump( EXCEPTION_POINTERS* pep )
{
// Open the file
HANDLE hFile = CreateFile( _T("MiniDump.dmp"), GENERIC_READ | GENERIC_WRITE,
0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
if( ( hFile != NULL ) && ( hFile != INVALID_HANDLE_VALUE ) )
{
// Create the minidump
MINIDUMP_EXCEPTION_INFORMATION mdei;
mdei.ThreadId = GetCurrentThreadId();
mdei.ExceptionPointers = pep;
mdei.ClientPointers = FALSE;
MINIDUMP_CALLBACK_INFORMATION mci;
mci.CallbackRoutine = (MINIDUMP_CALLBACK_ROUTINE)MyMiniDumpCallback;
mci.CallbackParam = 0; // this example does not use the context
MINIDUMP_TYPE mdt = MiniDumpNormal;
BOOL rv = MiniDumpWriteDump( GetCurrentProcess(), GetCurrentProcessId(),
hFile, mdt, (pep != 0) ? &mdei : 0, 0, &mci );
if( !rv )
_tprintf( _T("MiniDumpWriteDump failed. Error: %u \n"), GetLastError() );
else
_tprintf( _T("Minidump created.\n") );
// Close the file
CloseHandle( hFile );
}
else
{
_tprintf( _T("CreateFile failed. Error: %u \n"), GetLastError() );
}
}
BOOL CALLBACK MyMiniDumpCallback(
PVOID pParam,
const PMINIDUMP_CALLBACK_INPUT pInput,
PMINIDUMP_CALLBACK_OUTPUT pOutput
)
{
// Callback implementation
…
}
IncludeModuleCallback
当回调类型被设成IncludeModuleCallback,MiniDumpWriteDump询问回调函数是否要把特定可执行模块的信息存到minidump中。回调函数根据MINIDUMP_CALLBACK_INPUT的内容做出决定。此时,联合成员应该是MINIDUMP_INCLUDE_MODULE_CALLBACK:
typedef struct _MINIDUMP_INCLUDE_MODULE_CALLBACK {
ULONG64 BaseOfImage;
} MINIDUMP_INCLUDE_MODULE_CALLBACK, *PMINIDUMP_INCLUDE_MODULE_CALLBACK;
这里,BaseOfImage是模块在内存中的基地址。利用这个地址,可以获得模块更多的信息,以便决定是否需要存到minidump中。
回调函数利用返回值来把决定返回给MiniDumpWriteDump。如果回调返回值是TRUE,关于模块的信息会被包含进minidump中。通过后续的回调调用可以更精确的定义那些信息需要保存。如果返回值是FALSE,模块的所有信息会被丢弃。Minidump中看不到任何模块存在的痕迹。
对于这个回调类型,MINIDUMP_CALLBACK_OUTPUT没有用处。
ModuleCallback
一个模块通过了IncludeModuleCallback的测试之后,它会面临在通往minidump之路上的另外一个障碍。这个障碍是ModuleCallback。这个回调函数会决定关于这个模块的哪些信息需要保存。
这一次回调函数必须返回TRUE,来保证MiniDumpWriteDump继续工作。回调函数使用MINIDUMP_CALLBACK_OUTPUT结构体通知MiniDumpWriteDump的关于数据的决定。这个结构体中的联合包括一个ModuleWriteFlags成员。MiniDumpWriteDump会初始化它的值。它的值代表了可以保存在minidump中的各种模块信息。MODULE_WRITE_FLAGS枚举包含了所有可用的标志。
Figure 10:
typedef enum _MODULE_WRITE_FLAGS {
ModuleWriteModule = 0x0001,
ModuleWriteDataSeg = 0x0002,
ModuleWriteMiscRecord = 0x0004,
ModuleWriteCvRecord = 0x0008,
ModuleReferencedByMemory = 0x0010,
ModuleWriteTlsData = 0x0020,
ModuleWriteCodeSegs = 0x0040,
} MODULE_WRITE_FLAGS;
当MiniDumpWriteDump带着ModuleCallback参数调用回调函数,它会设置一些标志,告诉回调函数哪些模块信息可以包含在minidump中。回调函数可以分析这些标志,然后决定清除其中的一部分和还是全部。这样就可以告诉MiniDumpWriteDump哪些信息不需要保存。Figure 11中的表格列出了目前可用的所有标志,并且解释了他们所代表的信息。
Figure 11:
标志 |
描述 |
ModuleWriteModule |
这个标志允许从minidump中排除模块的所有信息。如果回调函数清除了这个标志,minidump中就不会包含这个模块的任何信息。 |
ModuleWriteCvRecord, ModuleWriteMiscRecord |
这些标志可以用来从minidump中排除模块的调试信息记录。如果清除这个标志,只有在开发机器是有这个模块的时候,调试器才能装载模块的调试信息。 |
ModuleWriteDataSeg |
这个标志可以用来从minidump中排除模块的数据段的内容。如果我们在MiniDumpWriteDump使用了MiniDumpWithDataSegs 标志,又希望选择哪些模块的数据段需要包含进来,这个标记就非常有用了。通常,我们希望看到所有我们自己模块的数据段(以便在调试器中看到全局变量),以及一小部分系统模块(比如,ntdll.dll)。其他第三方模块或者系统模块的数据段没有用处。由于可执行模块的数据段在minidump中占用了很大的空间。这个标记给我们提供一个很好的优化文件尺寸的机会。 |
ModuleWriteCodeSegs |
这个标记可以用来从minidump中排除模块的代码段。只有MiniDumpWithCodeSegs 传给MiniDumpWriteDump 函数的时候,这个标志才可用。这个标志可以用来选择哪些模块的代码段可以包含在minidump中。一定不要包含所有模块的代码段,这会显著增加minidump的大小。 |
ModuleReferencedByMemory |
这个标志需要和MINIDUMP_TYPE中的MiniDumpScanMemory一起使用。如果MiniDumpScanMemory被传给MiniDumpWriteDump,函数会遍历进程中的所有线程栈,查找指向可执行模块的地址空间的所有指针。搜索完成后,MiniDumpWriteDump就知道了哪些模块被引用了,哪些模块没有被引用。 |
ModuleWriteTlsData |
这个标志可能是用来控制模块的TLS数据(通过__declspec(thread)分配)是否要包括在mindump中。但是,到写这篇文章为止,还不能工作。 |
注意ModuleCallback只允许我们排除一些模块信息,但是不允许添加新的数据。这意味着,如果MiniDumpWriteDump没有设置相应的标志,在回调函数中设置相应的标志没有用处。例如,如果没有给MiniDumpWriteDump设置MiniDumpWithDataSegs标志,MiniDumpWriteDump函数就不会给任何模块设置ModuleWriteDataSeg标志。进一步,即使回调函数设置一个模块的ModuleWriteDataSeg标志,minidump中也不会真的包含模块数据段的内容。
在讨论很长时间MINIDUMP_CALLBACK_OUTPUT结构体之后,我们回头来看MINIDUMP_CALLBACK_INPUT结构体。这时候,这个联合会被解析成MINIDUMP_MODULE_CALLBACK结构体(Figure 12)。它里面包括了关于模块的丰富的信息,例如,名称和路径、大小、版本信息。
Figure 12:
typedef struct _MINIDUMP_MODULE_CALLBACK {
PWCHAR FullPath;
ULONG64 BaseOfImage;
ULONG SizeOfImage;
ULONG CheckSum;
ULONG TimeDateStamp;
VS_FIXEDFILEINFO VersionInfo;
PVOID CvRecord;
ULONG SizeOfCvRecord;
PVOID MiscRecord;
ULONG SizeOfMiscRecord;
} MINIDUMP_MODULE_CALLBACK, *PMINIDUMP_MODULE_CALLBACK;
IncludeThreadCallback
这个回调类型对于对于线程的作用,和IncludeModuleCallback对于模块的作用一样。这给我们一个机会来决定一个线程的哪些信息需要保存到minidump中。就像IncludeModuleCallback,回调函数返回TRUE表示要把线程信息保存到mindump,返回FASLE表示完全放弃这些信息。可以通过存储在MINIDUMP_CALLBACK_INPUT的ID来区分线程。
typedef struct _MINIDUMP_INCLUDE_THREAD_CALLBACK {
ULONG ThreadId;
} MINIDUMP_INCLUDE_THREAD_CALLBACK, *PMINIDUMP_INCLUDE_THREAD_CALLBACK;
MINIDUMP_CALLBACK_OUTPUT structure is not used.
ThreadCallback
这个回调类型的目的和ModuleCallback 对于模块的作用一样。回调类型的基本原则也一样。MINIDUMP_CALLBACK_OUTPUT中的联合包括了一系列的标志(ThreadWriteFlags),回调函数可以清除部分或者全部标记,来从minidump清除相应的线程信息。
MINIDUMP_CALLBACK_INPUT提供了很多种关于线程的信息。这里面的联合可以解释成MINIDUMP_THREAD_CALLBACK (Figure 13)。包括了线程ID和句柄、线程上下文、线程栈的边界。为了保证MiniDumpWriteDump继续运行,回调函数必须返回TRUE.
Figure 13:
typedef struct _MINIDUMP_THREAD_CALLBACK {
ULONG ThreadId;
HANDLE ThreadHandle;
CONTEXT Context;
ULONG SizeOfContext;
ULONG64 StackBase;
ULONG64 StackEnd;
} MINIDUMP_THREAD_CALLBACK, *PMINIDUMP_THREAD_CALLBACK;
Figure 14种表格列出了所有常用标志,以及他们所代表的信息。
Figure 14:
Flag |
Description |
ThreadWriteThread |
通过这个标志可以从minidump中清除一个线程的所有信息。如果回调函数清除了这个标志,所有其他的标志都会被忽略。Minidump就不保存任何关于这个线程的信息了。 |
ThreadWriteStack |
这个标志允许从minidump中清除线程栈的内容。因此,如果回调函数清除了这个标志,调试器就没办法看到线程的调用栈了。线程栈通常有几KB ,极少数情况可以达到几MB。因此这个标志会影响minidump的大小。 |
ThreadWriteContext |
通过这个标志可以清除线程上下文的内容(定义在winnt.h中的CONTEXT结构体)。如果回调清除了这个标志,调试器就不能看到线程上下文和调用栈,所有寄存器会被置成0。 |
ThreadWriteInstructionWindow |
通过这个标志可以清除线程指令窗口(当前执行指针附近的256字节)。如果清除这个标志,就没有办法直接看到出故障时的反汇编代码。如果想看到,就必须在开发者的计算机上装载相应的模块。 |
ThreadWriteThreadInfo |
只有给MiniDumpWriteDump 传递了MiniDumpWithThreadInfo 参数时,这个标志才被设置。通过这个标志,可以清除minidump中的额外线程信息。(参考本文中关于MiniDumpWithThreadInfo的解释) |
ThreadWriteThreadData |
只有给MiniDumpWriteDump 传递了MiniDumpWithProcessThreadData参数时,这个标志才被设置。通过这个标志可以从minidump中清除线程的特别信息(TEB的内容、TLS存储和一些附加信息) |
MemoryCallback
有些时候,我们肯能希望在minidump中添加一些额外内存区域的内容。例如,我们可能在堆上分配了一些数据(也可能是通过VirtualAlloc),希望在调试minidump的时候能够看到这些数据。我们可以通过MemoryCallback来完成这个功能。MiniDumpWriteDump会在通过回调调用处理完线程和模块之后调用这个回调函数。
当使用MemoryCallback 作为回调函数的回调参数时,MINIDUMP_CALLBACK_OUTPUT 中的联合会被解析成:
struct {
ULONG64 MemoryBase;
ULONG MemorySize;
};
如果回调函数在这个结构体中写入可读内存块的资质和大小,并且返回TRUE,这个内存块的内容就会被放到minidump中。我们可以添加多个内存块。当回调函数返回TRUE的时候,这个回调会被再次调用。MiniDumpWriteDump会一直等到返回FALSE才停止调用这个回调函数。
CancelCallback
MiniDumpWriteDump会定期调用这个回调类型。这个回调类型允许终止创建minidump的过程,这对于GUI应用程序很有用。MINIDUMP_CALLBACK_OUTPUT结构体被解析成两个值,Cancel和 CheckCancel:
struct {
BOOL CheckCancel;
BOOL Cancel;
};
如果我们希望彻底取消创建minidump,我们应该把Cancel设成TRUE。如果我们不想取消minidump,而只是不想再接收CancelCallback的回调,就把CheckCancel设成TRUE。如果两个成员都设置成FALSE,MiniDumpWriteDump就不再使用CancelCallback调用回调函数。
回调函数应该返回TRUE来确认MINIDUMP_CALLBACK_OUTPUT 的值被设置了。
回调的顺序
在讨论完回调的类型之后,我们可能会关心这些回调类型的顺序。调用的顺序如下:
- IncludeThreadCallback – 进程中的每一个线程一次
- IncludeModuleCallback –进程中每一个可执行模块一次
- ModuleCallback – 没有被IncludeModuleCallback排除的模块,每个调用一次
- ThreadCallback –没有被IncludeThreadCallback排除的线程,每个调用一次
- MemoryCallback会调用一次或者多次,一直到回调函数返回FALSE
另外,CancelCallback 会在其他回调类型之间定期调用。这样,保证在需要的时候可以中断minidump的创建过程。
这个例子程序(http://www.debuginfo.com/examples/src/effminidumps/CallbackOrder.cpp)会显示实际的调用顺序。你也可以使用MiniDump Wizard来测试各种回调(http://www.debuginfo.com/tools/minidumpwizard.html)。
MiniDump Wizard
你可以使用MiniDump Wizard 来试验各种minidump的选项并且看到他们会怎么影响minidump的大小和内容。MiniDump Wizard可以创建任意进程的minidump。它也可以模拟异常来创建自己的mindump文件。你可以选择把哪些类型标志 传递给MiniDumpWriteDump ,然后通过一系列的对话框对回调请求做出响应。
当创建完minidump,可以在一个调试器中装载它,然后查看包括了哪些信息。也可以使用MinDumpView(http://www.debuginfo.com/tools/minidumpview.html)应用来得到minidump中内容的清单。
用户数据流
除了MiniDumpWriteDump已经捕获的成功调试需要的所有应用程序状态之外,我们经常需要程序运行环境的一些额外信息。例如,如果可以查看配置文件的内容或者应用程序相关的注册表设置会很有帮助。Minidump允许把这些信息作为额外数据流添加进来。
这个例子程序显示了如何做到这一点(http://www.debuginfo.com/examples/src/effminidumps/WriteUserStream.cpp)。我们需要声明一个MINIDUMP_USER_STREAM_INFORMATION变量,在里面填充流的数量和用户数据流的指针数组。每个用户数据流用一个MINIDUMP_USER_STREAM结构体表示。结构体里面包括流的类型、大小、以及一个指向流数据的指针。流类型是识别流的一个唯一标志,必须是一个比LastReservedStream大的常数。
Figure 14:
typedef struct _MINIDUMP_USER_STREAM_INFORMATION {
ULONG UserStreamCount;
PMINIDUMP_USER_STREAM UserStreamArray;
} MINIDUMP_USER_STREAM_INFORMATION, *PMINIDUMP_USER_STREAM_INFORMATION;
typedef struct _MINIDUMP_USER_STREAM {
ULONG32 Type;
ULONG BufferSize;
PVOID Buffer;
} MINIDUMP_USER_STREAM, *PMINIDUMP_USER_STREAM;
当我们向一个minidump 添加了用户数据流,我们可以通过MiniDumpReadDumpStream 函数来读出这些信息。这个例子程序(http://www.debuginfo.com/examples/src/effminidumps/WriteUserStream.cpp)显示了如何从前一个例子(http://www.debuginfo.com/examples/src/effminidumps/WriteUserStream.cpp)写入的例子数据。
策略
MiniDumpWriteDump有丰富功能和大量的可用选项。这使得找到一个所有应用都适用的策略会很困难。对于每一个特定的情况,应用程序的开发者必须决定哪些选项对他们的调试工作有用。在这我会试着描述一些基本策略,用来解释如何把MiniDumpWriteDump的配置选项应用到真实场景中。我们会看到四种不同的MiniDumpWriteDump收集数据的策略。并且来了解他们会对minidump的大小和调试的可能性发生什么影响。
TinyDump
这不是一个真实世界的场景。这个方法显示了怎么样来创建一个最小可能数据集的minidump,来使它有一点用途。Figure 15总结了这种MiniDumpWriteDump配置选项。
Figure 15:
MINIDUMP_TYPE标志 |
MiniDumpNormal |
MiniDumpCallback |
IncludeThreadCallback – exclude all threads |
实现这种方式的例子程序在这个地址http://www.debuginfo.com/examples/src/effminidumps/TinyDump.cpp。
结果minidump非常小,在我的系统上非常小。并不令人惊讶,我们去掉了所有线程和模块的信息。如果你试着用WinDbg or VS.NET debugger来装载,你会发现调试器没有办法装载它。
但是,这个minidump还包含了异常的信息,所以不是完全无用,我们可以手工读取这些信息(使用MiniDumpReadDumpStream函数),可以看到异常发生的地址、异常时刻的线程上下文、异常代码甚至反汇编。你可以使用MinDumpView工具(http://www.debuginfo.com/tools/minidumpview.html)来查看其中的信息。为了保持工具简单,没有提供返汇编。
MiniDump
不像TinyDump,这种方式对于真实世界场景是有用的。它收集了足够的调试信息同时又保持minidump足够小。Figure 16中的表格描述了相应的MiniDumpWriteDump配置项。
Figure 16:
MINIDUMP_TYPE |
MiniDumpWithIndirectlyReferencedMemory, |
MiniDumpCallback |
IncludeThreadCallback – 包括所有线程 |
可以在这找到例子程序(http://www.debuginfo.com/examples/src/effminidumps/MiniDump.cpp)
结果的mindump文件仍然很小(在我的系统上大约40-50KB)。他比mindump的标准方式(MiniDumpNormal + no MiniDumpCallback))包含了更多的信息量。他允许查看栈上的引用的数据。为了优化大小,我们把所有线程栈没有引用的模块从minidump中去掉了(在我的系统上,advapi32.dll 和rpcrt4.dll被去掉了)。
但是,这个minidump还缺少一些重要的信息。例如,我们看不到全局标量的值,不能查看堆和TLS中分配的数据(除非他们被线程栈引用了)。
MidiDump
下一个方式会产生一个信息量充足的minidump,同时保证文件不会过大。Figure 17的表格描述了配置。
Figure 17:
MINIDUMP_TYPE flags |
MiniDumpWithPrivateReadWriteMemory, |
MiniDumpCallback |
IncludeThreadCallback –包括所有线程 |
例子程序可以在这看到(http://www.debuginfo.com/examples/src/effminidumps/MidiDump.cpp)。minidump的大小在我的系统上大约1350KB。当在调试器中装载的时候,我们可以得到应用程序的几乎所有信息,包括全局变量的值、堆和TLS的内容、PEB、TEB。我们甚至可以得到句柄信息以及虚拟内存布局。这是一个非常有用的dump,并且不是很大。下面的信息没有包括在mindump中:
- 所有模块的代码区(如果我们可以得到这些模块,就不需要他们)
- 某些模块的数据区(我们只包括了我们希望看到全局变量的模块的数据区)
MaxiDump
最后一个例子显示了如何创建一个包含所有可能数据的minidump。Figure 18的表格显示了如何做到这一点。
Figure 18:
MINIDUMP_TYPE flags |
MiniDumpWithFullMemory, |
MiniDumpCallback |
Not used |
例子程序可以在这找到http://www.debuginfo.com/examples/src/effminidumps/MaxiDump.cpp。
这个minidump对于这样一个简单程序来说已经很大了(在我的系统上有8MB)。但是,它给了我们在一个mindump中包含所有信息的可能。
对比
Figure 19的表格比较和四种方式创建的minidump的大小。除了这个例子程序的数据之外(它和真实程序会有一定差距),我还添加了一个真实程序的数据。同样也使用这四种不同的minidump。
Figure 19:
TinyDump |
MiniDump |
MidiDump |
MaxiDump |
|
例子程序 |
2 KB |
40-50 KB |
1,35 MB |
8 MB |
真实程序 |
2 KB |
200 KB |
14 MB |
35 MB |
补充
关于64位系统
这篇文章没有讨论MiniDumpWriteDump 中关于64位系统的选项。我的实验室里面没有64位的机器,我没有办法提供关于他们的更有效信息。
关于 DbgHelp版本
DbgHelp.dll一直在不断改进。新的特性会随着Debugging Tools for Windows工具包的新版本推出。在写这篇文章的时候,使用的版本是DbgHelp.dll 6.3。
例子程序
这篇文上涉及的所有例子程序(包括编译指令)可以在这找到。(http://www.debuginfo.com/examples/effmdmpexamples.html)
联系方式
Have questions or comments? Free free to contact Oleg Starodumov at firstname@debuginfo.com.
本文转自:http://blog.csdn.net/pkrobbie/article/details/6636310