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






4.90/5 (14投票s)
这是关于在 Windows 上调试应用程序的各种技术的第 3 部分,重点关注在应用程序崩溃时自动生成转储(Dump)的方法。
引言
本文是 Windows 调试技术系列文章的第 3 部分。本文着重介绍如何在应用程序本身中自动生成转储文件的过程。注意:这是一个系列文章,分为 5 个部分。
第一部分:Windows 调试技术 - 应用程序崩溃调试 (Windbg)
第二部分:Windows 调试技术 - 应用程序崩溃调试 (DebugDiag, AppVerifier)
第三部分:Windows 调试技术——调试应用程序崩溃(自动生成转储)
第四部分:
第五部分:
背景
正如您在前两部分中注意到的,我们是在需要时生成转储,这意味着我们需要重现问题,并且还需要将 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,设置符号并进行分析,您应该会看到以下内容。
所以我们在这里看到一些有趣的事实
- DEFAULT_BUCKET_ID: NULL_POINTER_WRITE,因为我们正在写入一个 NULL 指针
- CrashDump!func+2 [e:\study\windows internals\training\sample code\crashdump\crashdump\source.cpp @ 82]
这正是发生崩溃的行号
- 堆栈跟踪 (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:更新了转储的定义