허당 레몬도리

출처 : http://www.debuginfo.com/articles/effminidumps.html#minidumptypes

Introduction

In the last couple of years, crash dumps became an important part of our debugging activities. The possibility to create a snapshot of the application state at the exact moment of failure and analyze it with a conventional debugger running on the developer’s machine is invaluable when our software fails on the customer’s site or in the test lab. The first generation of crash dumps, often called “full user dumps”, captured the whole contents of the process’ virtual memory. While undoubtedly useful for post-mortem debugging, such dumps often became so huge that it was impossible, or at least inconvenient, to transfer them to the software developers electronically. In addition, there was no public API for creating such dumps programmatically, and we had to rely on external tools (such as Dr. Watson or Userdump) to create them.

A new family of crash dumps, called “minidumps”, came to us together with Windows XP. Minidumps are highly customisable. In the most popular configuration, a minidump contains just enough information to recover call stacks of all threads in the failed process, and to inspect the values of local variables at the moment of failure. Such dumps are small (usually only a couple of kilobytes), and therefore it is easy to transfer them electronically to the developers of the software. But if needed, a minidump can contain even more information than an old style crash dump (for example, minidumps can contain information about kernel objects used by the process). In addition, redistributable DbgHelp.dll exposes a public API for creating minidumps programmatically, and we do not have to rely on external tools anymore.

The customisable nature of minidumps brings us the question – how much information about the application’s state do we need to debug effectively and at the same time keep the minidumps as small as possible? While the knowledge of call stacks and values of local variables is usually enough to debug simple access violations, more difficult problems will require additional information. For example, we might need to look at the values of global variables, check integrity of the heap, or analyze the layout of the process’ virtual memory. At the same time, code sections of executable modules are likely to be redundant, if the executables themselves are available on the developer’s machine.

Fortunately, DbgHelp functions (MiniDumpWriteDump and MiniDumpCallback) give us such level of control, and even more. In this article, we will explore how to use these functions to create minidumps that are small but still contain enough information for effective debugging. We will also see what kinds of data can be included into the minidump, and how to use popular debuggers (WinDbg and VS.NET) to view this data.

Minidump types

Lets start with some code. Figure 1 contains the declaration of MiniDumpWriteDump function, and Figure 2 shows how to use this function to create a simple 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() ); 
  }

}

In this example, how do we specify what kinds of data should be included into the minidump? The answer is in the fourth parameter of MiniDumpWriteDump function, of type MINIDUMP_TYPE, whose definition is shown in 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;

What is interesting in MINIDUMP_TYPE enumeration is that it is actually a set of flags that allow us to get control over the contents of the minidump. Lets walk through the list of flags and see what they mean and how we can use them.

MiniDumpNormal

MiniDumpNormal is a special flag. Its value is 0, which means that this flag is always implicitly present, even if it is not specified explicitly. Therefore we can assume that this flag represents the basic set of data that is always present in minidumps (unless it is filtered out by a user-defined callback function, as we will see later).

The table in Figure 4 shows what kinds of data belong to this basic set:

Figure 4:

Data kindDescription
System informationInformation about the operating system and CPU, including:
  • Operating system version (including service pack)
  • Number of processors and their model
In WinDbg debugger, this information can be displayed with “vertarget” and “!cpuid” commands, respectively.
Process informationInformation about the process, including:
  • Process ID
  • Process times (creation time, and the time spent executing user and kernel code)
In WinDbg, process ID can be displayed using | (Process Status) command, and “.time” command can show process times.
Module informationFor every executable module loaded by the process, the following information is included:
  • Load address
  • Size of the module
  • File name (including path)
  • Version information (VS_FIXEDFILEINFO structure)
  • Module identity information that helps debuggers to locate the matching module and load debug information for it (checksum, timestamp, debug information record)
In WinDbg and VS.NET debuggers, we can use Modules window to view this information. In WinDbg, “lm” command can also be used.
Thread informationFor every thread running in the process, the following information is included:
  • Thread ID
  • Priority
  • Thread context
  • Suspend count
  • Address of the thread environment block (TEB) (but the contents of TEB are not included)
In VS.NET debugger, Threads window can show most parts of this information. In WinDbg, “~” command can be used.
Thread stacksFor every thread, the contents of its stack memory are included into the minidump. It allows us to obtain call stacks of the threads, inspect the values of function parameters and local variables.
Instruction windowFor every thread, 256 bytes of memory around the current instruction pointer are stored. It allows us to see the disassembly of the code the thread was executing at the moment of failure, even if the executable module itself is not available on the developer’s machine.
Exception informationException information can be included into the minidump via the fifth parameter of MiniDumpWriteDump function (as shown in Figure 2). In this case, the following information about the exception will be available:
  • Exception record (EXCEPTION_RECORD structure)
  • Thread context at the moment of the exception
  • Instruction window (256 bytes of memory around the address of the instruction that raised the exception)
When a minidump with exception information is loaded into VS.NET debugger, the debugger will automatically show the application state at the moment of the exception (including the call stack, register values, disassembled instruction and source code line which raised the exception). In WinDbg, we have to use .ecxr command to switch to the application state at the moment of the exception.

Yes, even the basic set of information available with MiniDumpNormal flag is very useful. We can locate the instruction that caused the failure, and we can also check the call stack to see how the thread came into that state. More information can be obtained by inspecting the values of function parameters and local variables, which are also available. In addition, this information is often enough to debug deadlocks, because we can look at call stacks of all threads and see what they are waiting on.

And all this useful information is available for a very attractive price – the size of the minidump is usually less than 20 kilobytes. The major factor that affects the size of the minidump is the size of the thread stacks – the more memory they occupy, the larger the minidump will be.

But if we try to use such minidump to debug more complex problems than a simple access violation or a deadlock, we quickly encounter the limits of the information collected by MiniDumpNormal flag. We might want to look at the value of a global variable, but it is not available. We might want to inspect the contents of a structure allocated on the heap, but no information about heaps is included into the minidump. And so on, until we realize that we need to include additional data into the minidump, and start looking at other members of MINIDUMP_TYPE enumeration.

MiniDumpWithFullMemory

This is probably the most popular flag after MiniDumpNormal. If it is specified, the contents of every readable page in the process address space will be included into the minidump. It gives us enormous debugging capabilities, because now we can look into any piece of memory allocated by the application. We can inspect the data stored on the stack, on the heap, in a module’s data section, and even in thread and process environment blocks, whose undocumented contents can sometimes give invaluable information for debugging.

The only problem with this flag is that the minidump can easily become huge (a couple of megabytes at the very least). In addition, the large part of the minidump’s contents is now redundant, because code sections of all executable modules are also included into the dump, but we can easily obtain the code from the executables themselves. Lets go back to MINIDUMP_TYPE enumeration and see if we can find a better option.

MiniDumpWithPrivateReadWriteMemory

If this flag is specified, the contents of every readable and writeable private memory page will be included into the minidump. It will allow us to inspect the data stored on the stack, on the heap, and even in TLS. The contents of PEB and TEBs are also included.

At the same time, the contents of shared memory pages are not included into the minidump (which means that we cannot inspect the contents of memory mapped files). Code and data sections of executable modules are not included too. It is good, because code sections do not occupy unnecessary space in the dump, but it is also bad because we cannot inspect the values of global variables.

Nevertheless, MiniDumpWithPrivateReadWriteMemory is a very useful option, especially if we combine it with some other options, as we will see later.

MiniDumpWithIndirectlyReferencedMemory

If this flag is specified, MiniDumpWriteDump function will scan the stack memory of every thread looking for pointers that point to other readable memory pages in the process’ address space. For every pointer found, 1024 bytes of memory around the location it points to will be stored in the minidump (256 bytes before and 768 bytes after).

Lets look at the sample code in 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; 
}

In this example, main function attempts to call A::Print via a null object pointer (pB->pA), which will result in an access violation at runtime. If we try to debug this problem using a minidump created with MiniDumpNormal flag only, we will not be able to look into the contents of B structure pointed by pB (because it is stored on the heap), and we will only have to guess why the object pointer passed to A::Print is null.

But if we specify MiniDumpWithIndirectlyReferencedMemory flag, MiniDumpWriteDump will notice that the stack contains a pointer (pB) into some area in the heap. 1024 bytes around the address stored in pB will be saved in the minidump, and therefore it will be possible to look at the contents of B structure in the debugger and see that pA is null.

Of course, MiniDumpWriteDump does not have access to debug information, and therefore it cannot distinguish between real pointers and values that point to some areas of memory only by coincidence. This situation is illustrated in 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; 
}

When function PrintSum causes an access violation, the sum of 0x10000 and 0x120 is stored on the stack at least once. The sum (0x10120) is not a pointer, but MiniDumpWriteDump has no way to know about it. If 0x10120 happens to be a valid address inside a readable memory page, 1024 bytes of memory (0x10020 – 0x10520) will be included into the minidump.

When scanning the stacks, MiniDumpWriteDump seems to ignore pointers that point into data sections of executable modules. As a result, MiniDumpWithIndirectlyReferencedMemory flag does not allow us to see the values of global variables, even if they are referenced from the stack. Probably this is because MINIDUMP_TYPE contains another flag for that purpose, as we will see soon.

With addition of MiniDumpWithIndirectlyReferencedMemory flag, the size of the minidump increases, and the amount of increase depends on the number of pointers found on the stacks.

MiniDumpWithDataSegs

If this flag is specified, the contents of all writeable data sections of all executable modules loaded by the process will be included into the minidump. If we want to inspect the values of global variables but do not want to resort to MiniDumpWithFullMemory flag, we have to use MiniDumpWithDataSegs.

The effect of this flag on the size of the minidump completely depends on the size of the corresponding data sections. Even in simple applications, it can add up to several hundred kilobytes, because data sections of system DLLs are also included. For example, .data section of DbgHelp.dll can contribute more than 100K, which is too much if we use this DLL only to call MiniDumpWriteDump. Later in this article I will show how we can ask MiniDumpWriteDump to include only those data sections that are really necessary.

MiniDumpWithCodeSegs

If this flag is specified, the contents of all code sections of all executable modules loaded by the process will be included into the minidump. As with MiniDumpWithDataSegs flag, the size of the minidump increases significantly. Later in this article I will show how we can customize MiniDumpWriteDump behavior so that only the necessary code sections are included.

MiniDumpWithHandleData

If this flag is specified, the minidump will contain information about all handles in the process handle table at the moment of failure. This information can be displayed with the help of !handle command in WinDbg debugger.

The impact of this flag on the size of the minidump depends on the number of handles in the process’ handle table.

MiniDumpWithThreadInfo

Additional information about threads in the process can be collected with the help of MiniDumpWithThreadInfo flag. For every thread, the following information will be available:

  • Thread times (creation time, and the time the thread spent executing user and kernel code)
  • Start address
  • Affinity

In WinDbg, thread times can be viewed with the help of .ttime command.

MiniDumpWithProcessThreadData

Sometimes we want to look at the contents of the process and thread environment blocks (PEB and TEBs). It can be done with the help of !peb and !teb commands in WinDbg, assuming that the memory occupied by these blocks is included into the minidump. This is exactly what MiniDumpWithProcessThreadData is provided for. If it is used, the contents of memory pages occupied by PEB and TEBs will be included into the minidump, as well as some other memory pages they reference (such as the places where environment variables and various process parameters are stored, as well as TLS slots allocated via TlsAlloc). Unfortunately, some data referenced by PEB and TEBs seems to be ignored (for example, TLS data allocated with __declspec(thread)), and we have to resort to MiniDumpWithFullMemory or MiniDumpWithPrivateReadWriteMemory flags if we need that data in the minidump.

MiniDumpWithFullMemoryInfo

If we want to be able to inspect the virtual memory layout of the process, we can use MiniDumpWithFullMemoryInfo flag. If it is specified, complete information about virtual memory layout of the process is included into the minidump, and can be displayed with !vadump and !vprot commands in WinDbg. The impact of this flag on the size of the minidump depends on the virtual memory layout – every region of pages with similar attributes (see VirtualQuery function for more information) will add 48 bytes to the minidump.

MiniDumpWithoutOptionalData

While all MINIDUMP_TYPE flags we have seen so far allowed us to add some data to the minidump, there are also flags that do just the opposite – they remove unnecessary information from the minidump. One of them, MiniDumpWithoutOptionalData, allows to minimize the amount of memory contents stored in the dump. If this flag is specified, only the memory specified by MiniDumpNormal flag is included, and other memory related options are suppressed even if they are explicitly specified (MiniDumpWithFullMemory, MiniDumpWithPrivateReadWriteMemory, MiniDumpWithIndirectlyReferencedMemory). At the same time, this flag does not change the behavior of the following flags: MiniDumpWithProcessThreadData, MiniDumpWithThreadInfo, MiniDumpWithHandleData, MiniDumpWithDataSegs, MiniDumpWithCodeSegs, MiniDumpWithFullMemoryInfo.

MiniDumpFilterMemory

If this flag is specified, the contents of stack memory, before saving into the minidump, are filtered so that only the data needed to reconstruct the call stack remains. All other data is overwritten with zeroes. As a result, call stacks can be reconstructed, but the values of all local variables and function parameters are set to 0.

This flag does not affect the size of the minidump, because it does not change the amount of memory stored there – it only overwrites parts of the data with zeroes. Also, this flag affects only the contents of memory occupied by thread stacks. Other pieces of memory (e.g. heaps) are not modified. Also, this flag has no effect if MiniDumpWithFullMemory option is used.

MiniDumpFilterModulePaths

This flag affects the module paths stored as part of the module information (see the description of MiniDumpNormal flag in this article). If it is specified, the module paths are removed from the dump, and only module names are available. According to the documentation, it is done to exclude potentially private information from the minidump (such as user names, which can be a part of module paths sometimes).

This flag has only a minor impact on the size of the minidump (since module paths are not the largest entities there). Debugging experience is not seriously affected too, because anyway we have to tell the debugger the location where matching executables are stored.

MiniDumpScanMemory

This flag allows us to save space in the minidump by excluding executable modules that are not needed to debug the problem. The flag works in close cooperation with MiniDumpCallback function, and therefore we have to take a look at that function first and then get back to MiniDumpScanMemory at appropriate time.

profile

허당 레몬도리

@LemonDory

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!