Effective minidump
Posted yangtopp
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Effective minidump相关的知识,希望对你有一定的参考价值。
Effective minidump (上)
原文更新: 07.02.2005
翻译:2011/7/16
目录
- 简介
- Minidump 类型
- MiniDumpCallback函数
- 用户数据流
- Dump类型
- 其他
- 例子程序
简介
在过去几年里,崩溃转储(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
以上是关于Effective minidump的主要内容,如果未能解决你的问题,请参考以下文章
使用 minidumps 和 GDB 分析 mingw 编译的可执行文件的崩溃?