有效的minidump(二)
函数MiniDumpCallback
如果要自定义MINIDUMP类型标志无法访问的MINIDUMP的内容,可以使用MiniDumpCallback函数。这是一个用户定义的回调,当MiniDumpWriteDump需要用户决定是否将某些数据包含到minidump中时,它将被调用。借助此功能,我们可以完成以下任务:
- 从minidump的模块信息中排除可执行模块(全部或部分)
- 从minidump的线程信息中排除线程(全部或部分)
- 将用户指定范围内存的内容包含到小型转储中
让我们看看MiniDumpCallback函数的声明
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结构的内容
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;
MiniDumpWriteDump对回调函数的请求由MINIDUMP_CALLBACK_INPUT结构制定。前两个成员的含义是显而易见的——它们包含为其创建小型转储的进程的id和句柄。第三个成员(callback type)包含请求的类型,自然称为回调类型。CallbackType的所有可能值都收集在MINIDUMP_CALLBACK_TYPE枚举中,我们将很快对它们进行更深入的研究。结构的第四个成员是union,其含义根据CallbackType的值而变化。联合包含有关MiniDumpWriteDump请求的其他数据。
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 … }
下面讲下MINIDUMP_CALLBACK_TYPE枚举:
- 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是内存中模块的基址,它可以用来获取有关该模块的更多信息,并决定是否需要在小型转储中使用它。回调函数使用其返回值将决策传递给MiniDumpWriteDump。如果回调返回TRUE,则有关模块的信息将包含在minidump中(在随后对回调函数的调用中可以进一步自定义此信息的确切内容)。如果回调返回FALSE,则有关模块的信息将被丢弃,并且在MIDIDUMP中不会看到模块的存在踪迹。MINIDUMP_CALLBACK_OUTPUT结构不用于此回调类型。
- ModuleCallback
当一个模块通过IncludeModuleCallback中的测试并存活下来后,它在进入小型转储时将面临另一个障碍。这个障碍是ModuleCallback,在这里回调函数可以决定应该包含哪些类型的模块信息。这次,回调函数的返回值必须为TRUE(以便让MiniDumpWriteDump继续),MINIDUMP_CALLBACK_OUTPUT结构用于将回调的决定传递给MiniDumpWriteDump。结构中的union被解释为有利于ModuleWriteFlags成员,该成员包含由MiniDumpWriteDump初始化的一组标志。这些标志表示可以包含到MIDIDUMP中的各种模块信息,并且当前存在的标志被收集在MODULE_WRITE_FLAGS 枚举中。
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不应包含哪些类型的信息。下面的表列出了当前可用的标志,并描述了它们所表示的信息类型:
注意,ModuleCallback只允许我们排除模块信息的某些部分,但不允许添加新数据。这意味着如果标记不是由MiniDumpWriteDump设置的,则在回调函数中设置它没有任何效果。例如,如果我们不将MiniDumpWithDataSegs标志传递给MiniDumpWriteDump,它将不会为任何模块设置ModuleWriteDataSeg标志。然后,即使回调函数将为模块设置ModuleWriteDataSeg标志,模块数据节的内容也不会包含在微型转储中。
在对MINIDUMP_CALLBACK_OUTPUT结构的内容进行了如此长的一次探索之后,让我们关注MINIDUMP_CALLBACK_INPUT。有趣的是,这次union被解释为MINIDUMP_MODULE_CALLBACK结构,它包含关模块的丰富信息集(例如名称和路径、大小、版本信息),如下: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以将有关线程的信息包含到小型转储中,或返回FALSE以完全丢弃此信息。线程可以通过其系统标识符来标识,该标识符存储在MINIDUMP_CALLBACK_INPUT的union中,如下所示:
typedef struct _MINIDUMP_INCLUDE_THREAD_CALLBACK { ULONG ThreadId; } MINIDUMP_INCLUDE_THREAD_CALLBACK, *PMINIDUMP_INCLUDE_THREAD_CALLBACK;
MINIDUMP_CALLBACK_OUTPUT结构不使用。
- ThreadCallback
这种回调类型对线程的作用与ModuleCallback对模块的作用相同,两种回调类型背后的基本原理也相同。MINIDUMP_CALLBACK_OUTPUT结构中的union被解释为一组标志(ThreadWriteFlags),回调函数可以清除其中的部分(或全部)以从MINIDUMP中排除线程信息的相应部分。
在MINIDUMP_CALLBACK_INPUT结构中提供了一组关于线程的丰富信息,其联合被解释为MINIDUMP_THREAD_CALLBACK。这些信息包括线程的标识符和句柄、线程上下文和线程堆栈的边界。回调函数必须返回TRUE才能让MiniDumpWriteDump继续。
typedef struct _MINIDUMP_THREAD_CALLBACK { ULONG ThreadId; HANDLE ThreadHandle; CONTEXT Context; ULONG SizeOfContext; ULONG64 StackBase; ULONG64 StackEnd; } MINIDUMP_THREAD_CALLBACK, *PMINIDUMP_THREAD_CALLBACK;
下面的表列出了最重要的标志,并描述了它们表示的信息类型
- MemoryCallback
有时我们希望将一些附加内存区域的内容包含到小型转储中。例如,如果我们在堆上分配一些数据(或者仅仅通过VirtualAlloc),并且希望在调试小型转储时看到这些数据。我们可以在MemoryCallback回调类型的帮助下完成它,MiniDumpWriteDump在使用上述回调类型收集线程和模块信息后调用该回调类型。当使用MemoryCallback作为回调类型调用回调函数时,MINIDUMP_CALLBACK_OUTPUT结构中的union被解释为:
struct { ULONG64 MemoryBase; ULONG MemorySize; };
如果回调函数用可读内存块的地址和大小填充此结构的成员并返回TRUE,则内存块的内容将保存在小型转储中。可以指定多个块,因为如果回调函数返回TRUE,它将被再次调用(当然是使用MemoryCallback回调类型)。MiniDumpWriteDump只有在返回FALSE后才会停止调用回调函数。
- CancelCallback
此回调类型(由MiniDumpWriteDump定期调用)允许取消创建minidump的过程,这在GUI应用程序中非常有用。MINIDUMP_CALLBACK_OUTPUT结构中的union被解释为两个值:Cancel和CheckCancel:
struct { BOOL CheckCancel; BOOL Cancel; };
如果要完全取消创建小型转储,应将cancel设置为TRUE。如果我们现在不想取消,但希望以后接收CancelCallback回调,则CheckCancel应设置为TRUE。如果两个成员都设置为FALSE,MiniDumpWriteDump将不再调用CancelCallback回调类型的回调函数。回调函数应返回TRUE以确认它在MINIDUMP_CALLBACK_OUTPUT结构中设置的值。
回调顺序
在我们讨论了回调类型之后,可以很有趣地看到MiniDumpWriteDump使用它们的顺序:
- IncludeThreadCallback – 过程中每个线程一次
- IncludeModuleCallback – 过程中的每个可执行模块一次
- ModuleCallback – 对IncludeModuleCallback未排除的每个模块执行一次
- ThreadCallback – 对IncludeThreadCallback未排除的每个线程执行一次
- MemoryCallback是调用一次或多次,直到回调函数返回FALSE
此外,在其他回调类型之间定期使用cancelcallback类型,以便在必要时可以取消小型转储创建过程。
MiniDump向导
您可以使用MiniDump向导应用程序来尝试各种MiniDump选项,并查看它们如何影响MiniDump的大小和内容。小型转储向导可以为任意进程创建小型转储,还可以模拟异常并为小型转储向导本身创建小型转储。您可以选择将哪些小型转储类型标志传递给小型转储writedump函数,并在一系列对话框中响应回调请求。
创建小型转储后,将其加载到调试器中,并检查可用的信息类型。您还可以使用MiniDumpView应用程序获取minidump中可用的各种信息的列表。
用户数据流
虽然MiniDumpWriteDump捕获的应用程序状态对于成功调试至关重要,但我们通常需要有关应用程序运行环境的其他信息。例如,查看配置文件的内容或检查注册表中特定于应用程序的设置可能会很有用。MiniDumpWriteDump允许我们将此信息作为附加数据流包含到minidump中。
我们应该声明MINIDUMP_USER_STREAM_INFORMATION类型的变量,并用流的数量和指向用户数据流数组的指针填充其内容。每个用户数据流由MINIDUMP_USER_STREAM结构描述,该结构包含流类型(它用作流的唯一标识符,并且必须大于LastReservedStream常量)、大小和指向流数据的指针。结构如下所示。
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函数读取它。
策略
MiniDumpWriteDump的丰富特性和大量可用选项使得很难选择对各种应用程序都同样有效的策略。在每种特定情况下,应用程序的开发人员都必须决定哪些选项对他们的调试任务有用。在这里,我将尝试描述一些基本策略,展示如何在实际场景中应用MiniDumpWriteDump配置选项的知识。我们将研究使用MiniDumpWriteDump收集数据的四种不同方法,并了解它们如何影响minidump的大小以及有效调试的可能性。
TinyDump
这实际上不是一个真实的场景。相反,这种方法显示可以包含在小型转储中的最小可能的数据集,以使其至少稍微有用。MiniDumpWriteDump配置选项总结在下面的表中。
如果您尝试将小型转储加载到WinDbg或VS.NET调试器中,您将看到调试器无法加载它。但小型转储并不是完全无用,因为它仍然包含有关异常的信息。我们可以手动读取此信息(使用MiniDumpReadDumpStream函数),并查看发生异常的地址、发生异常时的线程上下文、异常代码,甚至反汇编。您可以使用MiniDumpView工具来显示此信息(除了反汇编之外,为了保持简单)。
MiniDump
与TinyDump不同,这种方法可以在实际场景中使用,以收集足够的调试信息,并且仍然保持最小转储大小尽可能小。下面的表描述了MiniDumpWriteDump配置选项。
MidiDump
这种方法将产生一个信息量非常大的小型转储,其大小仍将保持远离变得巨大。下面的表描述了这种配置。
在我的系统上,小型转储的大小约为1350千字节。当将其加载到调试器中时,我们可以获得几乎所有关于应用程序的信息,包括全局变量的值、堆和TLS、PEB、TEBs的内容。我们甚至可以获取句柄信息并检查虚拟内存的布局。一个非常有用的垃圾场,事实上,它并不太大,因为我们仍然关心它的大小-不包括以下信息:
- 所有模块的代码段(因为如果我们可以获得模块本身,就不需要它们)
- 一些模块的数据段(我们只为那些我们真正想在调试器中看到全局变量的模块包含数据段)
MaxiDump
创建包含所有可能信息集的小型转储。下面的表显示了如何实现这一点。
即使对于一个简单的应用程序,小型转储也是很大的。但它让我们可以访问各种可能的信息,这些信息可以包含在一个小型垃圾场中。