上一篇文章讲述了在MSYS2和Linux里,进行崩溃信息导出的方法。对MSVC来说,除了导出出错文本外,还可以用API直接导出可以进行调试的 dmp 文件。相关代码参考Gitcode.
文章目录
- [1. 头文件](#1. 头文件)
- [2. 测试程序](#2. 测试程序)
- [3. MSVC 的Release版本配置](#3. MSVC 的Release版本配置)
- [4. 发布程序并收集错误](#4. 发布程序并收集错误)
- [5. 发回本地调试错误](#5. 发回本地调试错误)
- [6. 注意事项](#6. 注意事项)
1. 头文件
延续上一篇文章的接口,驱动AI编程,竟然直接获得了接口完全一致的Win32 API 程序:
cpp
#pragma once
// 必要的系统头文件
#include <Windows.h>
#include <DbgHelp.h>
#include <time.h>
#include <string>
#include <vector>
#include <sstream>
#include <iomanip>
// 链接DbgHelp库(MSVC专用)
#pragma comment(lib, "Dbghelp.lib")
// 禁用安全警告(localtime_s已替代不安全的localtime)
#pragma warning(disable: 4996)
namespace CRASHDMP_CPP {
// 异常代码转可读字符串
inline std::string ExceptionCodeToString(DWORD exceptionCode)
{
switch (exceptionCode)
{
case EXCEPTION_ACCESS_VIOLATION: return "EXCEPTION_ACCESS_VIOLATION (非法内存访问)";
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED (数组越界)";
case EXCEPTION_BREAKPOINT: return "EXCEPTION_BREAKPOINT (断点异常)";
case EXCEPTION_DATATYPE_MISALIGNMENT: return "EXCEPTION_DATATYPE_MISALIGNMENT (数据类型未对齐)";
case EXCEPTION_FLT_DENORMAL_OPERAND: return "EXCEPTION_FLT_DENORMAL_OPERAND (浮点非正规操作数)";
case EXCEPTION_FLT_DIVIDE_BY_ZERO: return "EXCEPTION_FLT_DIVIDE_BY_ZERO (浮点除零)";
case EXCEPTION_FLT_INEXACT_RESULT: return "EXCEPTION_FLT_INEXACT_RESULT (浮点不精确结果)";
case EXCEPTION_FLT_INVALID_OPERATION: return "EXCEPTION_FLT_INVALID_OPERATION (浮点无效操作)";
case EXCEPTION_FLT_OVERFLOW: return "EXCEPTION_FLT_OVERFLOW (浮点溢出)";
case EXCEPTION_FLT_STACK_CHECK: return "EXCEPTION_FLT_STACK_CHECK (浮点栈检查)";
case EXCEPTION_FLT_UNDERFLOW: return "EXCEPTION_FLT_UNDERFLOW (浮点下溢)";
case EXCEPTION_ILLEGAL_INSTRUCTION: return "EXCEPTION_ILLEGAL_INSTRUCTION (非法指令)";
case EXCEPTION_IN_PAGE_ERROR: return "EXCEPTION_IN_PAGE_ERROR (页错误)";
case EXCEPTION_INT_DIVIDE_BY_ZERO: return "EXCEPTION_INT_DIVIDE_BY_ZERO (整数除零)";
case EXCEPTION_INT_OVERFLOW: return "EXCEPTION_INT_OVERFLOW (整数溢出)";
case EXCEPTION_INVALID_DISPOSITION: return "EXCEPTION_INVALID_DISPOSITION (无效处置)";
case EXCEPTION_NONCONTINUABLE_EXCEPTION: return "EXCEPTION_NONCONTINUABLE_EXCEPTION (不可继续异常)";
case EXCEPTION_PRIV_INSTRUCTION: return "EXCEPTION_PRIV_INSTRUCTION (特权指令)";
case EXCEPTION_SINGLE_STEP: return "EXCEPTION_SINGLE_STEP (单步执行)";
case EXCEPTION_STACK_OVERFLOW: return "EXCEPTION_STACK_OVERFLOW (栈溢出)";
default:
char buf[64] = { 0 };
sprintf_s(buf, "未知异常代码 (0x%08X)", exceptionCode);
return buf;
}
}
// 获取系统信息(OS版本、位数等)
inline std::string GetSystemInfoString()
{
std::ostringstream oss;
// 获取系统位数
SYSTEM_INFO sysInfo = { 0 };
GetSystemInfo(&sysInfo);
oss << "系统位数: " << (sysInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64 ? "x64" : "x86") << "\n";
// 进程/线程ID
oss << "进程ID: " << GetCurrentProcessId() << "\n";
oss << "线程ID: " << GetCurrentThreadId() << "\n";
return oss.str();
}
// 解析调用堆栈(核心新增函数)
inline std::string GetCallStackString(EXCEPTION_POINTERS* pExceptionPointers)
{
if (pExceptionPointers == nullptr) return "调用堆栈:无异常上下文\n";
std::ostringstream oss;
HANDLE hProcess = GetCurrentProcess();
HANDLE hThread = GetCurrentThread();
CONTEXT* pContext = pExceptionPointers->ContextRecord;
SYSTEM_INFO sysInfo = { 0 };
GetSystemInfo(&sysInfo);
// ========== 1. 初始化符号引擎 ==========
// 符号路径:优先本地符号,其次系统符号服务器(自动下载)
// srv*C:\Symbols*https://msdl.microsoft.com/download/symbols 表示:
// 本地缓存到C:\Symbols,远程从微软符号服务器下载
const char* szSymbolPath = "srv*C:\\Symbols*https://msdl.microsoft.com/download/symbols";
if (!SymInitialize(hProcess, szSymbolPath, TRUE))
{
oss << "调用堆栈:符号引擎初始化失败 (错误码: " << GetLastError() << ")\n";
return oss.str();
}
// 设置符号选项(提升解析成功率)
SymSetOptions(SYMOPT_DEFERRED_LOADS | SYMOPT_UNDNAME | SYMOPT_LOAD_LINES);
// ========== 2. 初始化堆栈遍历结构 ==========
STACKFRAME64 stackFrame = { 0 };
DWORD machineType = 0;
// 区分x86/x64架构
#ifdef _WIN64
machineType = IMAGE_FILE_MACHINE_AMD64;
stackFrame.AddrPC.Offset = pContext->Rip;
stackFrame.AddrPC.Mode = AddrModeFlat;
stackFrame.AddrFrame.Offset = pContext->Rbp;
stackFrame.AddrFrame.Mode = AddrModeFlat;
stackFrame.AddrStack.Offset = pContext->Rsp;
stackFrame.AddrStack.Mode = AddrModeFlat;
#else
machineType = IMAGE_FILE_MACHINE_I386;
stackFrame.AddrPC.Offset = pContext->Eip;
stackFrame.AddrPC.Mode = AddrModeFlat;
stackFrame.AddrFrame.Offset = pContext->Ebp;
stackFrame.AddrFrame.Mode = AddrModeFlat;
stackFrame.AddrStack.Offset = pContext->Esp;
stackFrame.AddrStack.Mode = AddrModeFlat;
#endif
oss << "调用堆栈(帧序号 | 地址 | 函数名 | 模块名 | 偏移):\n";
int frameIndex = 0;
const int MAX_FRAMES = 64; // 最多解析64层堆栈(避免无限循环)
// ========== 3. 遍历堆栈帧 ==========
while (StackWalk64(
machineType,
hProcess,
hThread,
&stackFrame,
pContext,
nullptr,
SymFunctionTableAccess64,
SymGetModuleBase64,
nullptr
) && frameIndex < MAX_FRAMES)
{
// 跳过无效的PC地址
if (stackFrame.AddrPC.Offset == 0) break;
oss << " 帧" << std::setw(2) << frameIndex << ": ";
oss << "0x" << std::hex << std::uppercase << stackFrame.AddrPC.Offset << " | ";
// ========== 4. 解析符号信息 ==========
DWORD64 dwDisplacement = 0;
char szFuncName[2048] = "未知函数";
char szModuleName[MAX_PATH] = "未知模块";
SYMBOL_INFO* pSymbol = (SYMBOL_INFO*)malloc(sizeof(SYMBOL_INFO) + 2048);
if (pSymbol != nullptr)
{
memset(pSymbol, 0, sizeof(SYMBOL_INFO) + 2048);
pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO);
pSymbol->MaxNameLen = 2047;
// 解析地址对应的符号
if (SymFromAddr(hProcess, stackFrame.AddrPC.Offset, &dwDisplacement, pSymbol))
{
strcpy_s(szFuncName, sizeof(szFuncName), pSymbol->Name);
// 解析模块名
IMAGEHLP_MODULE64 moduleInfo = { 0 };
moduleInfo.SizeOfStruct = sizeof(IMAGEHLP_MODULE64);
if (SymGetModuleInfo64(hProcess, stackFrame.AddrPC.Offset, &moduleInfo))
{
strcpy_s(szModuleName, sizeof(szModuleName), moduleInfo.ModuleName);
}
}
free(pSymbol);
}
// 输出解析结果(函数名 + 模块名 + 偏移)
oss << szFuncName << " | " << szModuleName << " | +0x" << std::hex << dwDisplacement << "\n";
frameIndex++;
}
if (frameIndex == 0)
{
oss << " 无有效堆栈帧\n";
}
// ========== 5. 清理符号引擎 ==========
SymCleanup(hProcess);
return oss.str();
}
class CrashDumper;
// 全局CrashDumper实例指针(用于静态回调函数访问)
static CrashDumper* g_pGlobalCrashDumper = nullptr;
class CrashDumper
{
public:
// 构造函数:接收dump文件前缀(如D:\log\myApp)
explicit CrashDumper(const std::string& dumpFilePrefix)
: m_dumpFilePrefix(dumpFilePrefix)
{
// 保存原始错误模式并设置静默崩溃(不弹出系统错误框)
m_originalErrorMode = SetErrorMode(SEM_NOGPFAULTERRORBOX);
// 保存原始异常过滤器并设置自定义过滤器
m_originalExceptionFilter = SetUnhandledExceptionFilter(UnhandledExceptionHandler);
}
// 析构函数:还原系统原始设置
~CrashDumper()
{
// 还原异常过滤器
SetUnhandledExceptionFilter(m_originalExceptionFilter);
// 还原错误模式
SetErrorMode(m_originalErrorMode);
}
// 禁用拷贝构造和赋值(单例语义,避免重复设置过滤器)
CrashDumper(const CrashDumper&) = delete;
CrashDumper& operator=(const CrashDumper&) = delete;
private:
// MiniDump级别优先级:从最详细到最小(逐一尝试)
static const std::vector<MINIDUMP_TYPE> s_miniDumpTypes;
// MiniDump级别名称映射(用于TXT日志)
static const std::vector<std::string> s_miniDumpTypeNames;
// 记录最终成功的DMP级别
std::string m_successfulDumpType;
// 文件名前缀(如D:\log\myApp)
std::string m_dumpFilePrefix;
// 系统原始设置(用于析构还原)
static LPTOP_LEVEL_EXCEPTION_FILTER m_originalExceptionFilter;
static UINT m_originalErrorMode;
// 生成带时间戳的dump文件名
std::string GenerateDumpFileName() const
{
// 获取当前系统时间
time_t now = time(nullptr);
tm localTime{};
localtime_s(&localTime, &now);
// 格式化时间戳:YYYYMMDD.HHMMSS
char timeStamp[32] = { 0 };
strftime(timeStamp, sizeof(timeStamp), "%Y%m%d.%H%M%S", &localTime);
// 拼接完整文件名:前缀.crashdump.时间戳.dmp
return m_dumpFilePrefix + ".crash_log_" + timeStamp + ".dmp";
}
// 生成对应TXT日志文件名(DMP路径 + .txt)
std::string GenerateTxtFileName(const std::string& dumpPath) const
{
return dumpPath + ".txt";
}
// 写入崩溃详情到TXT文件(新增调用堆栈)
bool WriteCrashInfoToTxt(const std::string& txtPath, EXCEPTION_POINTERS* pExceptionPointers)
{
if (pExceptionPointers == nullptr) return false;
// 创建TXT文件(ANSI编码,避免乱码)
HANDLE hFile = CreateFileA(
txtPath.c_str(),
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
nullptr
);
if (hFile == INVALID_HANDLE_VALUE) return false;
// 构建日志内容
std::ostringstream oss;
oss << "==================== 程序崩溃详情 ====================\n";
// 1. 崩溃时间(精确到毫秒)
SYSTEMTIME sysTime = { 0 };
GetLocalTime(&sysTime);
oss << "崩溃时间: "
<< sysTime.wYear << "-" << std::setw(2) << std::setfill('0') << sysTime.wMonth << "-" << std::setw(2) << sysTime.wDay
<< " " << std::setw(2) << sysTime.wHour << ":" << std::setw(2) << sysTime.wMinute << ":" << std::setw(2) << sysTime.wSecond
<< "." << std::setw(3) << sysTime.wMilliseconds << "\n";
// 2. DMP文件信息
oss << "DMP文件路径: " << GenerateDumpFileName() << "\n";
oss << "成功生成的DMP级别: " << m_successfulDumpType << "\n\n";
// 3. 系统信息
oss << "==================== 系统信息 ====================\n";
oss << GetSystemInfoString() << "\n";
// 4. 异常详情
oss << "==================== 异常详情 ====================\n";
EXCEPTION_RECORD* pExceptionRecord = pExceptionPointers->ExceptionRecord;
oss << "异常类型: " << ExceptionCodeToString(pExceptionRecord->ExceptionCode) << "\n";
oss << "异常代码: 0x" << std::hex << std::uppercase << pExceptionRecord->ExceptionCode << "\n";
oss << "异常地址: 0x" << std::hex << std::uppercase << (DWORD64)pExceptionRecord->ExceptionAddress << "\n";
oss << "异常参数数量: " << pExceptionRecord->NumberParameters << "\n";
for (DWORD i = 0; i < pExceptionRecord->NumberParameters; ++i)
{
oss << "异常参数" << i + 1 << ": 0x" << std::hex << std::uppercase << pExceptionRecord->ExceptionInformation[i] << "\n";
}
// 5. 线程上下文(简化版,仅关键寄存器)
oss << "\n==================== 线程上下文(关键寄存器) ====================\n";
CONTEXT* pContext = pExceptionPointers->ContextRecord;
#ifdef _WIN64
oss << "RAX: 0x" << std::hex << std::uppercase << pContext->Rax << "\n";
oss << "RBX: 0x" << std::hex << std::uppercase << pContext->Rbx << "\n";
oss << "RCX: 0x" << std::hex << std::uppercase << pContext->Rcx << "\n";
oss << "RDX: 0x" << std::hex << std::uppercase << pContext->Rdx << "\n";
oss << "RIP: 0x" << std::hex << std::uppercase << pContext->Rip << "\n";
oss << "RSP: 0x" << std::hex << std::uppercase << pContext->Rsp << "\n";
#else
oss << "EAX: 0x" << std::hex << std::uppercase << pContext->Eax << "\n";
oss << "EBX: 0x" << std::hex << std::uppercase << pContext->Ebx << "\n";
oss << "ECX: 0x" << std::hex << std::uppercase << pContext->Ecx << "\n";
oss << "EDX: 0x" << std::hex << std::uppercase << pContext->Edx << "\n";
oss << "EIP: 0x" << std::hex << std::uppercase << pContext->Eip << "\n";
oss << "ESP: 0x" << std::hex << std::uppercase << pContext->Esp << "\n";
#endif
// 6. 调用堆栈(核心新增)
oss << "\n==================== 调用堆栈 ====================\n";
oss << GetCallStackString(pExceptionPointers) << "\n";
// 写入文件
std::string logContent = oss.str();
DWORD bytesWritten = 0;
BOOL writeResult = WriteFile(
hFile,
logContent.c_str(),
(DWORD)logContent.length(),
&bytesWritten,
nullptr
);
// 关闭文件句柄
CloseHandle(hFile);
// 验证写入长度
return writeResult && (bytesWritten == logContent.length());
}
// 尝试写入MiniDump(按级别逐一尝试)
bool TryWriteMiniDump(const std::string& dumpPath, EXCEPTION_POINTERS* pExceptionPointers)
{
// 构造MiniDump异常信息结构体(关键修正点)
MINIDUMP_EXCEPTION_INFORMATION exceptionInfo = { 0 };
exceptionInfo.ThreadId = GetCurrentThreadId(); // 当前崩溃线程ID
exceptionInfo.ExceptionPointers = pExceptionPointers; // 异常指针
exceptionInfo.ClientPointers = TRUE; // 客户端指针标识
// 遍历所有MiniDump级别(从详细到简单)
for (size_t i = 0; i < s_miniDumpTypes.size(); ++i)
{
MINIDUMP_TYPE dumpType = s_miniDumpTypes[i];
const std::string& dumpTypeName = s_miniDumpTypeNames[i];
// 创建dump文件(处理权限问题,允许读写共享)
HANDLE hFile = CreateFileA(
dumpPath.c_str(),
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
nullptr
);
// 文件创建失败,直接退出
if (hFile == INVALID_HANDLE_VALUE)
{
return false;
}
// 尝试写入MiniDump(修正参数5为异常信息结构体)
BOOL dumpResult = MiniDumpWriteDump(
GetCurrentProcess(),
GetCurrentProcessId(),
hFile,
dumpType,
pExceptionPointers ? &exceptionInfo : nullptr, // 正确传入结构体指针
nullptr,
nullptr
);
// 关闭文件句柄
CloseHandle(hFile);
// 写入成功则返回
if (dumpResult)
{
m_successfulDumpType = dumpTypeName;
// 生成TXT文件(失败不影响主流程)
WriteCrashInfoToTxt(GenerateTxtFileName(dumpPath), pExceptionPointers);
return true;
}
}
// 所有级别都失败,清理0字节文件
CleanupEmptyFile(dumpPath);
return false;
}
// 清理0字节的dump文件
void CleanupEmptyFile(const std::string& filePath)
{
// 获取文件大小
HANDLE hFile = CreateFileA(
filePath.c_str(),
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr
);
if (hFile != INVALID_HANDLE_VALUE)
{
DWORD fileSize = GetFileSize(hFile, nullptr);
CloseHandle(hFile);
// 如果文件大小为0则删除
if (fileSize == 0)
{
DeleteFileA(filePath.c_str());
}
}
}
// 未处理异常的回调函数(静态成员)
static LONG WINAPI UnhandledExceptionHandler(EXCEPTION_POINTERS* pExceptionPointers)
{
// 获取CrashDumper实例(注:需确保全局唯一实例,此处通过全局变量实现)
//extern CrashDumper* CRASHDMP_MSVC::g_pGlobalCrashDumper;
if (g_pGlobalCrashDumper == nullptr)
{
ExitProcess(EXIT_FAILURE);
}
// 生成dump文件名
std::string dumpPath = g_pGlobalCrashDumper->GenerateDumpFileName();
// 尝试写入dump文件
g_pGlobalCrashDumper->TryWriteMiniDump(dumpPath, pExceptionPointers);
// 静默退出程序
ExitProcess(EXIT_FAILURE);
// 返回值无实际意义(已调用ExitProcess)
return EXCEPTION_EXECUTE_HANDLER;
}
};
// 静态成员初始化
LPTOP_LEVEL_EXCEPTION_FILTER CrashDumper::m_originalExceptionFilter = nullptr;
UINT CrashDumper::m_originalErrorMode = 0;
// MiniDump级别定义(从最详细到最小)
const std::vector<MINIDUMP_TYPE> CrashDumper::s_miniDumpTypes = {
MiniDumpWithFullMemory, // 最详细:包含完整内存
MiniDumpWithFullMemoryInfo, // 完整内存信息
MiniDumpWithHandleData, // 包含句柄数据
MiniDumpWithThreadInfo, // 包含线程信息
MiniDumpWithUnloadedModules, // 包含已卸载模块信息
MiniDumpNormal // 最小级别:仅基本信息
};
// MiniDump级别名称映射(与上面顺序一致)
const std::vector<std::string> CrashDumper::s_miniDumpTypeNames = {
"MiniDumpWithFullMemory (完整内存)",
"MiniDumpWithFullMemoryInfo (完整内存信息)",
"MiniDumpWithHandleData (句柄数据)",
"MiniDumpWithThreadInfo (线程信息)",
"MiniDumpWithUnloadedModules (已卸载模块)",
"MiniDumpNormal (基础信息)"
};
// 便捷初始化函数(建议在程序入口处调用)
inline void init_crash_handler(const std::string& dumpFilePrefix)
{
static CrashDumper crashDumper(dumpFilePrefix);
g_pGlobalCrashDumper = &crashDumper;
}
}
2. 测试程序
测试程序依旧采用上一篇的例子:
cpp
#include <stdio.h>
#include "crash_dump_msvc.h"
void func_c(int value) {
int* null_ptr = NULL;
null_ptr[value] = value; // 崩溃点(第 113 行)
}
void func_b(int value) {
func_c(value * 2); // 调用 func_c(第 117 行)
}
void func_a(int value) {
func_b(value + 3); // 调用 func_b(第 121 行)
}
int main(int /*argc*/, char * argv[]) {
// 初始化崩溃处理器(必须在程序启动初期调用)
CRASHDMP_CPP::init_crash_handler(argv[0]);
puts("Program started...\n");
puts("Target= crash_log_xxx.txt\n");
// 触发崩溃(函数调用链:main -> func_a -> func_b -> func_c)
func_a(10);
return 0;
}
3. MSVC 的Release版本配置
要注意,在项目里设置正确的编译开关。要确认的位置有两处,一个是编译的C++选项常规选项卡,调试选项选取"程序数据库":

另一个,是链接器选取生成调试信息。

这两个选项只会增加EXE的大小,产生pdb文件,并不会显著拖慢运行速率。
4. 发布程序并收集错误
把编译好的程序拷贝到生产环境,一旦崩溃,就会产生两个文件:
其中,TXT文件如下:
txt
==================== 程序崩溃详情 ====================
崩溃时间: 2025-12-06 11:08:31.856
DMP文件路径: D:\crash_dump.exe.crash_log_20251206.110831.dmp
成功生成的DMP级别: MiniDumpWithFullMemory (完整内存)
==================== 系统信息 ====================
系统位数: x64
进程ID: 15172
线程ID: 8156
==================== 异常详情 ====================
异常类型: EXCEPTION_ACCESS_VIOLATION (非法内存访问)
异常代码: 0xC0000005
异常地址: 0x7FF70B773EF7
异常参数数量: 2
异常参数1: 0x1
异常参数2: 0x68
==================== 线程上下文(关键寄存器) ====================
RAX: 0x0
RBX: 0x151D8CEC070
RCX: 0x39D15AFC80
RDX: 0x151D8AB0000
RIP: 0x7FF70B773EF7
RSP: 0x39D15AFCE0
==================== 调用堆栈 ====================
调用堆栈(帧序号 | 地址 | 函数名 | 模块名 | 偏移):
帧 0: 0x7FF70B773EF7 | main | crash_dump | +0x127
帧 1: 0x7FF70B775514 | __scrt_common_main_seh | crash_dump | +0x10C
帧 2: 0x7FF98E0AE8D7 | BaseThreadInitThunk | KERNEL32 | +0x17
帧 3: 0x7FF98F70C53C | RtlUserThreadStart | ntdll | +0x2C
另一个文件是 dmp文件,要通过邮件传回开发者。
5. 发回本地调试错误
开发者从邮件拿到dmp文件后,直接用Visual Studio打开:

而后选择"仅使用本机调试"

即可定位到错误

6. 注意事项
如果当前的代码已经重新修改编译了,或者换了绝对路径,那拿回来的dmp就无法关联到GUI的报错位置(上图)了。因此,一般发布包建议把代码拷贝到一个专门的发布代码镜像文件夹,编译发布。发布后,文件夹就冻结了。所有的修改都在原有文件夹修改。
这也和使用Git管理代码很像,一个Tag建立后,比如 Release V 1.2.0,则Tag不再改动了。