簡介
在過去幾年裏,崩潰轉儲(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 [email protected].
本文轉自:http://blog.csdn.net/pkrobbie/article/details/6636310