Effective minidump (下)
前半部分 (http://blog.csdn.net/pkrobbie/article/details/6636310)
MiniDumpCallback函数
如果MINIDUMP_TYPE不能满足我们定制minidump内容的需要,我们可以使用MiniDumpCallback函数。这是一个用户定义的回调函数,MiniDumpWriteDump会调用它,让用户来决定是否把某些数据放到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 的值被设置了。
回调的顺序
在讨论完回调的类型之后,我们可能会关心这些回调类型的顺序。调用的顺序如下:
另外,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 [email protected].
这份文件翻译自
http://www.debuginfo.com/articles/effminidumps.html
http://www.debuginfo.com/articles/effminidumps2.html
[补充]
如果希望找到一个完整的实现,可以使用XCrashReport
http://www.codeproject.com/Articles/5257/XCrashReport-Exception-Handling-and-Crash-Reportin
转载,请注明原文和译文的出处