Effective minidump

Posted yangtopp

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Effective minidump相关的知识,希望对你有一定的参考价值。

Effective minidump (上)

原文更新: 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。


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)对应的一些最终重要的请求,以及回调函数如何对他们做出响应。在开始之前

以上是关于Effective minidump的主要内容,如果未能解决你的问题,请参考以下文章

Effective Python 中文版

《Effective C++ 》学习笔记——条款11

《Effective C++》学习笔记

Effective C++ 条款45

[读书笔记]Effective Java 第一章

《Effective C++》读书笔记汇总