Effective minidump (上)

【轉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)的信息,包括:

  • 進程ID
  • 進程時間(創建時間,用戶態和核心態的執行時間)

WinDbg通過| (Process Status)命令顯示進程ID,“.time”顯示進程時間。

模塊(Module) 信息

對於進程裝載的所有可執行模塊,顯示如下信息:

  • 裝載地址
  • 模塊的大小
  • 文件名(包括路徑)
  • 版本信息(VS_FIXEDFILEINFO structure)
  • 模塊識別信息,幫助調試器定位相應的模塊並且裝載調試信息 (校驗和,時間戳,調試信息記錄)

在WinDbg和 VS.NET中,可以在Modules窗口中看到這些信息。WinDbg的“lm”也可以看到這些信息。

線程信息

對於進程中的任何一個線程,會包括這些信息:

  • 線程ID
  • 優先級
  • 線程上下文
  • 暫停計數(Suspend count)
  • 線程環境塊(thread environment block ,TEB)的地址,但是不包括TEB的內容

VS.NET中,Threads窗口中可以顯示大多數這些信息。WinDbg中用 “~”命令顯示線程信息。

線程棧

對於每一個線程,minidump包含了棧內存的內容。允許我們得到所有線程的調用棧,查看函數參數和局部變量的值。

指令窗口

對於每一線程,當前指令指針前後的256自己內存會保留下來。允許我們即使沒有可執行模塊,也可以獲得故障時刻的線程代碼的反編譯信息。

異常信息

可以通過MiniDumpWriteDump 函數的第5個參數(見Figure 2)把異常信息包含到minidump中。這種情況下minidump會包括如下異常信息:

  • 異常記錄 (EXCEPTION_RECORD structure)
  • 異常發生時刻的線程上下文
  • 指令窗口(發生異常的指令地址附近的256字節)

當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。

 

原文摘自 http://www.debuginfo.com/articles/effminidumps.html

發佈了8 篇原創文章 · 獲贊 1 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章