【轉http://blog.csdn.net/pkrobbie/article/details/6636310】
原文更新: 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。