65.9K
CodeProject 正在变化。 阅读更多。
Home

第三部分:Windows 调试技术——调试应用程序崩溃(自动生成转储)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (14投票s)

2014年1月9日

CPOL

5分钟阅读

viewsIcon

49110

这是关于在 Windows 上调试应用程序的各种技术的第 3 部分,重点关注在应用程序崩溃时自动生成转储(Dump)的方法。

引言

本文是 Windows 调试技术系列文章的第 3 部分。本文着重介绍如何在应用程序本身中自动生成转储文件的过程。

注意:这是一个系列文章,分为 5 个部分。
第一部分:Windows 调试技术 - 应用程序崩溃调试 (Windbg)
第二部分:Windows 调试技术 - 应用程序崩溃调试 (DebugDiag, AppVerifier)
第三部分:Windows 调试技术——调试应用程序崩溃(自动生成转储)
第四部分: Windows 调试技术 - 调试内存泄漏( Perfmon)
第五部分: Windows 调试技术 - 调试内存泄漏( CRT API)

背景

正如您在前两部分中注意到的,我们是在需要时生成转储,这意味着我们需要重现问题,并且还需要将 procdump 放置在目标计算机上以获取转储。在生产系统上,客户通常不同意在系统上安装任何应用程序。另一方面,我们可能无法再次遇到该问题,它可能是间歇性的,但仍然是生产环境中的问题,而客户无法提供重现问题的确切步骤。这对于产品管理来说可能是一个非常棘手的情况。如果应用程序崩溃时能够自行生成崩溃转储,供以后收集分析,那将是非常棒的。

定义

 转储文件是应用程序在获取转储时那一刻的快照。它显示了当时正在执行的进程以及已加载的模块。转储主要用于调试开发人员无法访问的计算机上出现的问题。例如,当您无法在自己的计算机上重现客户的崩溃或挂起时,可以使用客户计算机上的转储文件。 

 转储文件有两种类型

1. 小型转储 (Mini Dump):仅存储进程信息的子集,通常包含与线程堆栈、进程环境等相关的信息。小型转储的大小通常较小,一般为 1.5 MB 或更少。

2. 完全转储 (Full Dump):存储有关进程虚拟内存的所有信息,例如线程、调用堆栈、堆状态、已加载库。基本上,进程的所有相关信息都可用。完全转储的大小与进程的虚拟内存一样大,通常为几百 MB 甚至 GB。

为了分析进程中出现的问题,最好拥有一个完全转储,小型转储在某些情况下也可能有用。

 让我们一步步了解如何实现自转储生成。

源代码

异常是一种意外情况,遇到时会导致应用程序退出。因此,我们应该密切关注此异常。我们如何做到这一点?在 Windows 平台上有一个 API。

LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(
  _In_  LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);

此 API 将在给定进程内的任何线程中处理任何未处理的异常时被调用。此 API 应在应用程序的第一个入口点调用(通常在非托管代码中是 main())。此 API 需要一个参数,该参数是一个异常处理程序,定义了在这种情况下应做什么。所以现在代码看起来会是这样的:

int main()
{
 SetUnhandledExceptionFilter( unhandled_handler);
}

LONG CALLBACK unhandled_handler(EXCEPTION_POINTERS* e)
{
    return EXCEPTION_CONTINUE_SEARCH;
}
现在我们需要决定在 `unhandled_handler` 中做什么,在本例中是创建转储。
int main()
{
 SetUnhandledExceptionFilter( unhandled_handler);
}

LONG CALLBACK unhandled_handler(EXCEPTION_POINTERS* e)
{
	make_minidump(e);
    return EXCEPTION_CONTINUE_SEARCH;
}

void make_minidump(EXCEPTION_POINTERS* e)
{
	TCHAR tszFileName[MAX_BUFF_SIZE] = {0};
	TCHAR tszPath[MAX_BUFF_SIZE] = {0};
	SYSTEMTIME stTime = {0};
	GetSystemTime(&stTime);
	SHGetSpecialFolderPath(NULL,tszPath, CSIDL_APPDATA, FALSE);
	StringCbPrintf(tszFileName, 
			       _countof(tszFileName), 
				   _T("%s\\%s__%4d%02d%02d_%02d%02d%02d.dmp"), 
				   tszPath, _T("CrashDump"),  
				   stTime.wYear, 
				   stTime.wMonth, 
				   stTime.wDay, 
				   stTime.wHour, 
				   stTime.wMinute, 
				   stTime.wSecond);

    HANDLE hFile = CreateFile(tszFileName, GENERIC_WRITE, FILE_SHARE_READ, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
    if(hFile == INVALID_HANDLE_VALUE)
        return;

    MINIDUMP_EXCEPTION_INFORMATION exceptionInfo;
    exceptionInfo.ThreadId = GetCurrentThreadId();
    exceptionInfo.ExceptionPointers = e;
    exceptionInfo.ClientPointers = FALSE;

    MiniDumpWriteDump(
        GetCurrentProcess(),
        GetCurrentProcessId(),
        hFile,
        MINIDUMP_TYPE(MiniDumpWithIndirectlyReferencedMemory | MiniDumpScanMemory | MiniDumpWithFullMemory),
        e ? &exceptionInfo : NULL,
        NULL,
        NULL);

	if(hFile)
	{
		CloseHandle(hFile);
		hFile = NULL;
	}
    return; 
}

因此,现在在 `unhandled_handler` 函数中,我们调用 `make_minidump`,它实际上会写入转储。`unhandled_handler` 函数是一个回调函数,它接收 `EXCEPTION_POINTERS`。此结构包含基于生成它的机器的异常信息。`EXCEPTION_POINTERS` 被传递给 `make_minidump` 函数,该函数实际上将转储写入磁盘。`make_minidump` 函数的第一部分只是根据应用程序名称和时间戳创建文件名。然后使用“CreateFile”创建文件。

"MINIDUMP_EXCEPTION_INFORMATION" 结构使用从父函数接收到的 `CurrentThreadID` 和异常指针进行填充。然后调用 `MiniDumpWriteDump` 函数。这里有趣的一个参数是 `MINIDUMP_TYPE`。如果我们查看该 API 的 msdn 帮助文档,会有一个相当大的列表,但根据我的分析,我添加了三个标志,其中第三个是最重要的,即 `MiniDumpWithFullMemory`。这提供了主内存中特定进程的所有引用位置,这对于调试非常有帮助。

有了完整的内存转储,转储的大小会非常大,但这确实有助于分析转储。通过类似的流程,您可以探索更多关于转储类型的信息,并根据您的需求进行更改。让我们举一个例子,看看它是如何工作的。

#include <Windows.h>
#include <Dbghelp.h>
#include <iostream>
#include <tchar.h>
#include <strsafe.h>
#include <shlobj.h>

#pragma comment(lib, "DbgHelp")
#define MAX_BUFF_SIZE 1024

using namespace std;
DWORD WINAPI func();

void make_minidump(EXCEPTION_POINTERS* e)
{
	TCHAR tszFileName[MAX_BUFF_SIZE] = {0};
	TCHAR tszPath[MAX_BUFF_SIZE] = {0};
	SYSTEMTIME stTime = {0};
	GetSystemTime(&stTime);
	SHGetSpecialFolderPath(NULL,tszPath, CSIDL_APPDATA, FALSE);
	StringCbPrintf(tszFileName, 
			       _countof(tszFileName), 
				   _T("%s\\%s__%4d%02d%02d_%02d%02d%02d.dmp"), 
				   tszPath, _T("CrashDump"),  
				   stTime.wYear, 
				   stTime.wMonth, 
				   stTime.wDay, 
				   stTime.wHour, 
				   stTime.wMinute, 
				   stTime.wSecond);

    HANDLE hFile = CreateFile(tszFileName, GENERIC_WRITE, FILE_SHARE_READ, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
    if(hFile == INVALID_HANDLE_VALUE)
        return;

    MINIDUMP_EXCEPTION_INFORMATION exceptionInfo;
    exceptionInfo.ThreadId = GetCurrentThreadId();
    exceptionInfo.ExceptionPointers = e;
    exceptionInfo.ClientPointers = FALSE;

    MiniDumpWriteDump(
        GetCurrentProcess(),
        GetCurrentProcessId(),
        hFile,
        MINIDUMP_TYPE(MiniDumpWithIndirectlyReferencedMemory | MiniDumpScanMemory | MiniDumpWithFullMemory),
        e ? &exceptionInfo : NULL,
        NULL,
        NULL);

	if(hFile)
	{
		CloseHandle(hFile);
		hFile = NULL;
	}
    return; 
}

LONG CALLBACK unhandled_handler(EXCEPTION_POINTERS* e)
{
    make_minidump(e);
    return EXCEPTION_CONTINUE_SEARCH;
}

int main()
{
    SetUnhandledExceptionFilter( unhandled_handler);
	DWORD dwThreadID = 0;
	HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)func, NULL, 0, &dwThreadID);
	if(hThread == NULL)
	{
		return 1;
	}
	WaitForSingleObject(hThread, INFINITE);
	return 0;
    
}

DWORD WINAPI func()
{
	int *p = NULL;
	*p = 10;
	return 0;
}

这是来自 第 1 部分 的同一个示例,只是有一个轻微的改动,增加了一个会导致崩溃的线程。执行此代码后,转储文件将在 `%Appdata%` 文件夹中生成。启动 Windbg,设置符号并进行分析,您应该会看到以下内容。

所以我们在这里看到一些有趣的事实

  1. DEFAULT_BUCKET_ID: NULL_POINTER_WRITE,因为我们正在写入一个 NULL 指针
  2. CrashDump!func+2 [e:\study\windows internals\training\sample code\crashdump\crashdump\source.cpp @ 82]

    这正是发生崩溃的行号

  3. 堆栈跟踪 (Stack Trace)
    00000080`26ddf7f8 00007ffb`87c115cd : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : CrashDump!func+0x2
    00000080`26ddf800 00007ffb`884c43d1 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0xd
    00000080`26ddf830 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x1d
    这提供了故障线程的堆栈跟踪,而不是从 main 开始的。

摘要

因此,通过这些,我们现在知道如何将代码添加到我们的应用程序中,该代码将在应用程序崩溃时生成转储。根据经验法则,这应该是任何正在开发的应用程序的一部分。

历史 

  • 2014-01-09:文章上传
  • 2014-01-20:更新了到其他部分的链接  
  • 2014-01-27:更新了小型转储和完全转储的定义
  • 2014-01-30:更新了转储的定义 
© . All rights reserved.