MSYS2下使用libbacktrace为MINGW编译器Release模式导出崩溃堆栈

大家知道,windows下可以通过VC的coredump导出dmp格式的错误堆栈,并进行离线调试。但是,MSYS2编译器却不好生成pdb文件。

近期,遇到一个诡异的错误,很难在客户那边复现。没办法,必须解决Mingw编译器的崩溃堆栈输出机制。经过AI加自身测试,很快解决,现分享如下。相比unwind和windows API,这个输出的内容是最友好的。

文章目录

  • [1. 安装backtrace库](#1. 安装backtrace库)
  • [2. 创建工具头文件](#2. 创建工具头文件)
  • [3. 在main中开启功能](#3. 在main中开启功能)
  • [4. 修改编译选项允许Release保留调试信息](#4. 修改编译选项允许Release保留调试信息)
  • [5. 运行效果](#5. 运行效果)

1. 安装backtrace库

libbacktrace是msys2环境下官方提供的调试信息处理库,主要用于生成和分析程序崩溃时的调用栈信息。该库是GNU Binutils工具集的一部分,专门为MinGW和MSYS2环境进行了优化适配。

主要特性包括:

  1. 支持多种调试信息格式(DWARF、PDB等)
  2. 提供符号化堆栈回溯功能
  3. 支持跨平台调试信息解析
  4. 与GDB调试器深度集成

在MSYS2中的典型安装方式:

bash 复制代码
pacman -S mingw-w64-x86_64-libbacktrace

我们全部安装

bash 复制代码
pacman -S ucrt64/mingw-w64-ucrt-x86_64-libbacktrace mingw64/mingw-w64-x86_64-libbacktrace clang64/mingw-w64-clang-x86_64-libbacktrace

这个库常见应用场景:

  • 程序崩溃时自动生成调用栈
  • 配合异常处理机制记录错误上下文
  • 开发调试工具时解析二进制文件符号
  • 性能分析工具中的调用路径追踪

该库特别适合在Windows平台下配合MinGW工具链使用,可以完美处理PE格式可执行文件的调试信息。相比Windows原生调试工具,libbacktrace提供了更符合Unix/Linux开发者习惯的API接口。

2. 创建工具头文件

这个头文件在 main.cpp里引用1次即可, 文件名:crash_dump_mingw.h

cpp 复制代码
#ifndef CRASH_DUMP_MINGW_H
#define CRASH_DUMP_MINGW_H


#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <string.h>
#include <backtrace.h>  // libbacktrace 核心头文件
#include <windows.h>    // Windows 平台辅助(获取程序路径)

namespace CRASHDMP_MINGW {
// ------------------------------
// 全局变量声明(静态化,避免多文件包含冲突)
// ------------------------------
// libbacktrace 状态句柄(初始化后复用)
static struct backtrace_state * g_backtrace_state = NULL;
// 崩溃日志文件指针(仅在崩溃时使用)
static FILE* g_crash_log = NULL;
static const char* g_log_prefix = ".";
// ------------------------------
// 前置声明(避免编译警告)
// ------------------------------
static void backtrace_error_cb(void* data, const char* msg, int errnum);
static int backtrace_frame_cb(void* data, uintptr_t pc, const char* filename, int line, const char* function);
void crash_handler(int sig);
void init_crash_handler_once(void);
void init_crash_handler(const char* log_prefix);  // 扩展:自定义日志路径

// ------------------------------
// 扩展接口:自定义崩溃日志保存路径(可选调用)
// 示例:init_crash_handler_with_path("D:/crash_logs");
// ------------------------------
void init_crash_handler(const char* log_prefix) {
	if (log_prefix)
		g_log_prefix = log_prefix;
	init_crash_handler_once();  // 调用核心初始化
}

// ------------------------------
// 1. libbacktrace 错误回调函数(符号解析失败时触发)
// ------------------------------
static void backtrace_error_cb(void* data, const char* msg, int errnum) {
	(void)data;  // 忽略未使用参数,避免编译警告
	// 输出错误信息到日志(日志打开失败则输出到 stderr)
	const char* err_msg = (errnum != 0) ? strerror(errnum) : "NORMAL";
	fprintf_s(g_crash_log ?: stderr, "BACKTRACE ERROR:%s(CODE:%d,DETAIL:%s)\n", msg, errnum, err_msg);
}

// ------------------------------
// 2. libbacktrace 栈帧回调函数(每个栈帧触发一次)
// ------------------------------
static int backtrace_frame_cb(void* data, uintptr_t pc, const char* filename, int line, const char* function) {
	(void)data;  // 忽略未使用参数
	static int stack_depth = 0;  // 栈深度计数(仅初始化一次)

	// 格式化输出栈帧信息(兼容 C/C++,避免格式警告)
	fprintf_s(g_crash_log, "[%d] PC: 0x%lx | FUNC: %s | POS: %s:%d\n",
			stack_depth++,
			(unsigned long)pc,
			function ? function : "unknown",  // 空指针安全访问
			filename ? filename : "unknown",
			line > 0 ? line : 0);

	return 0;  // 返回 0 继续回溯下一个栈帧
}

// ------------------------------
// 3. 崩溃信号处理函数(捕获常见崩溃信号)
// ------------------------------
void crash_handler(int sig) {
	// 信号名称映射表(扩展更多常见信号,兼容 MinGW64 环境)
	const char* sig_name = NULL;
	switch (sig) {
	case SIGSEGV: sig_name = "SIGSEGV (段错误/访问非法地址)"; break;
	case SIGABRT: sig_name = "SIGABRT (断言失败/主动终止)";   break;
	case SIGILL:  sig_name = "SIGILL (非法指令)";             break;
	case SIGFPE:  sig_name = "SIGFPE (浮点错误/除零)";        break;
	//case SIGBUS:  sig_name = "SIGBUS (总线错误/地址对齐错误)"; break;
	case SIGINT:  sig_name = "SIGINT (用户中断/Ctrl+C)";      break;
	case SIGTERM: sig_name = "SIGTERM (进程终止信号)";        break;
	//case SIGQUIT: sig_name = "SIGQUIT (退出信号/Ctrl+\\)";    break;
	default:      sig_name = "Unknown Signal";                break;
	};

	// 全局变量:日志保存路径(默认当前目录,可通过扩展接口修改)
	time_t now = time(NULL);
	char log_date[16384] = {0};
	char log_filename[16384] = {0};
	strftime(log_date, sizeof(log_date),
			 ".crash_log_%Y%m%d_%H%M%S.txt", localtime(&now));
	strcpy_s(log_filename,g_log_prefix);
	strcat_s(log_filename,log_date);

	// 打开日志文件(追加模式,若目录不存在则尝试创建)
	g_crash_log = fopen(log_filename, "a");

	// 日志文件仍打开失败,输出到 stderr
	if (!g_crash_log) {
		g_crash_log = stderr;
		fprintf_s(g_crash_log, "Warning, Log file creation Error ( PATH=%s), stderr is activated instead.\n", log_filename);
	} else {
		// 打印日志文件路径到控制台(方便用户查找)
		fprintf_s(stdout, "CrashLog created : %s\n", log_filename);
	}

	// 写入崩溃头部信息(兼容 C/C++ 字符串格式)
	fprintf_s(g_crash_log, "=========================================\n");
	fprintf_s(g_crash_log, "程序崩溃时间:%s", ctime(&now));
	fprintf_s(g_crash_log, "崩溃信号:%d(%s)\n", sig, sig_name);
	fprintf_s(g_crash_log, "堆栈回溯信息(函数名+行号,需编译时加 -g):\n");

	// 执行栈回溯(skip=1 跳过当前 crash_handler 栈帧)

	/*
extern int backtrace_full (struct backtrace_state *state, int skip,
			   backtrace_full_callback callback,
			   backtrace_error_callback error_callback,
			   void *data);*/
	if (g_backtrace_state != NULL) {
		backtrace_full(g_backtrace_state, 1, backtrace_frame_cb, backtrace_error_cb, (void *)0);
	} else {
		fprintf_s(g_crash_log, "error:libbacktrace is not inited.\n");
	}

	// 收尾:关闭文件+退出程序(避免无限信号循环)
	fprintf_s(g_crash_log, "=========================================\n");
	if (g_crash_log != stderr) {
		fclose(g_crash_log);
	}
	g_crash_log = NULL;  // 重置全局指针
	exit(EXIT_FAILURE);
}
LONG WINAPI crash_exception_handler(PEXCEPTION_POINTERS /*exception_info*/)
{
	crash_handler(0);
	return EXCEPTION_EXECUTE_HANDLER;
}
// ------------------------------
// 4. 核心初始化接口(必须在程序启动时调用)
// 功能:初始化 libbacktrace + 注册崩溃信号处理器
// ------------------------------
void init_crash_handler_once(void) {
	// 初始化 libbacktrace(线程安全模式,自动获取程序路径)
	g_backtrace_state = backtrace_create_state(NULL, 1, backtrace_error_cb, NULL);

	if (!g_backtrace_state) {
		fprintf_s(stderr, "Warning:libbacktrace Initial Failed.\n");
		fprintf_s(stderr, "Possible reason:1. no libbacktrace found. 2. gcc -g not selected. 3. gcc -Wl,-s is enabled in linking time. \n");
		return;
	}

	// 注册常见崩溃信号的处理器(MinGW64 兼容)
	signal(SIGSEGV, crash_handler);  // 段错误
	signal(SIGABRT, crash_handler);  // 断言失败
	signal(SIGILL,  crash_handler);  // 非法指令
	signal(SIGFPE,  crash_handler);  // 浮点错误
	//signal(SIGBUS,  crash_handler);  // 总线错误
	signal(SIGINT,  crash_handler);  // 中断信号(可选,根据需求启用)
	signal(SIGTERM, crash_handler);  // 终止信号(可选)

// Windows 平台额外注册 SEH 异常处理(捕获更多 Windows 特有异常)
	SetUnhandledExceptionFilter(crash_exception_handler);


	fprintf_s(stdout, "CRASGDUMP is online: please enable -g in compile time, and disable -Wl,-s in linking.\n");
}



}


#endif // CRASH_DUMP_MINGW_H

3. 在main中开启功能

直接弄一个测试用的main.cpp

cpp 复制代码
#include <stdio.h>
#include "crash_dump_mingw.h"

void func_c(int value) {
	int* null_ptr = NULL;
	null_ptr[value] = value; // 崩溃点
}

void func_b(int value) {
	func_c(value * 2); // 调用 func_c
}

void func_a(int value) {
	func_b(value + 3); // 调用 func_b
}

int main(int /*argc*/, char * argv[]) {
	// 初始化崩溃处理器(必须在程序启动初期调用)
	CRASHDMP_MINGW::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;
}

4. 修改编译选项允许Release保留调试信息

编译开关保留

-g -gpubnames -gstrict-dwarf -fno-inline-small-functions -fno-omit-frame-pointer

这些编译选项主要用于增强调试信息的生成和优化控制,具体作用如下:

  1. -g

    基础调试选项,生成标准的调试信息(DWARF格式),包含变量名、函数名等基本信息。

  2. -gpubnames

    在调试信息中额外生成公共名称表(Pubnames Section),加速调试器对符号的查找。例如在GDB中通过info functions命令时能更快检索函数列表。

  3. -gstrict-dwarf

    强制生成严格符合DWARF标准的调试信息,禁用编译器特有的扩展格式。适用于需要跨工具链兼容的场景,如使用第三方DWARF分析工具时。

  4. -fno-inline-small-functions

    禁止编译器对小函数进行内联优化。调试时特别有用:

    • 保留完整的函数调用栈
    • 确保断点能正确设置在小型函数内
    • 典型场景:调试模板函数或频繁调用的工具函数
  5. -fno-omit-frame-pointer

    强制保留帧指针寄存器(如x86的EBP/RBP):

    • 始终生成标准的栈帧结构
    • 保证backtrace能完整显示调用链
    • 性能影响:约3-5%的额外开销,但对内存读写密集型代码影响更小

典型应用场景:

  • 内核模块调试(需完整栈帧)
  • 性能分析工具(如perf)采样
  • 复杂崩溃现场的栈回溯
  • 与ASan/LSan等内存检测工具配合使用

注意:在GCC 8+版本中,部分选项(如-gpubnames)已被整合到-g的增强模式(-ggnu-pubnames)。

链接开关去掉选项 -Wl,-s

在编译链接时,-Wl,-s 是一个传递给链接器的选项,用于告诉链接器在生成可执行文件时去掉符号表(symbol table)和重定位信息(relocation information)。这会减小最终生成的可执行文件的大小,但也会导致调试更加困难。

必须去掉 -Wl,-s 选项

QMAKE设置

qmake 复制代码
LIBS += -lbacktrace

QMAKE_CFLAGS_RELEASE += -g -gpubnames -gstrict-dwarf -fno-inline-small-functions -fno-omit-frame-pointer
QMAKE_CXXFLAGS_RELEASE += -g -gpubnames -gstrict-dwarf -fno-inline-small-functions -fno-omit-frame-pointer
QMAKE_LFLAGS_RELEASE -= -Wl,-s

5. 运行效果

运行:

txt 复制代码
21:46:11: Starting D:\User\Documents\testUnwind\build\Desktop_Qt_6_10_1_shared_MinGW_w64_UCRT64_MSYS2-Release\release\testUnwind.exe...
CRASGDUMP is online: please enable -g in compile time, and disable -Wl,-s in linking.
Program started...

Target= crash_log_xxx.txt

CrashLog created : D:\User\Documents\testUnwind\build\Desktop_Qt_6_10_1_shared_MinGW_w64_UCRT64_MSYS2-Release\release\testUnwind.exe.crash_log_20251201_214611.txt

输出文件:

txt 复制代码
=========================================
程序崩溃时间:Mon Dec  1 21:46:11 2025
崩溃信号:11(SIGSEGV (段错误/访问非法地址))
堆栈回溯信息(函数名+行号,需编译时加 -g):
[0] PC: 0x518322f1 | FUNC: _gnu_exception_handler | POS: D:/W/B/src/mingw-w64/mingw-w64-crt/crt/crt_handler.c:209
[1] PC: 0x5beb156e | FUNC: unknown | POS: unknown:0
[2] PC: 0x5e4863fe | FUNC: unknown | POS: unknown:0
[3] PC: 0x5e332326 | FUNC: unknown | POS: unknown:0
[4] PC: 0x5e485d3d | FUNC: unknown | POS: unknown:0
[5] PC: 0x51831904 | FUNC: _Z6func_ci | POS: ../../main.cpp:6
[6] PC: 0x51831904 | FUNC: _Z6func_bi | POS: ../../main.cpp:10
[7] PC: 0x51832dcc | FUNC: _Z6func_ai | POS: ../../main.cpp:14
[8] PC: 0x51832dcc | FUNC: main | POS: ../../main.cpp:25
[9] PC: 0x518310c8 | FUNC: __tmainCRTStartup | POS: D:/W/B/src/mingw-w64/mingw-w64-crt/crt/crtexe.c:236
[10] PC: 0x51831415 | FUNC: mainCRTStartup | POS: D:/W/B/src/mingw-w64/mingw-w64-crt/crt/crtexe.c:122
[11] PC: 0x5ca9e8d6 | FUNC: unknown | POS: unknown:0
[12] PC: 0x5e3ac53b | FUNC: unknown | POS: unknown:0
[13] PC: 0xffffffff | FUNC: unknown | POS: unknown:0
=========================================

可以看到,正确获得了三次调用:

txt 复制代码
[5] PC: 0x51831904 | FUNC: _Z6func_ci | POS: ../../main.cpp:6
[6] PC: 0x51831904 | FUNC: _Z6func_bi | POS: ../../main.cpp:10
[7] PC: 0x51832dcc | FUNC: _Z6func_ai | POS: ../../main.cpp:14
[8] PC: 0x51832dcc | FUNC: main | POS: ../../main.cpp:25
相关推荐
FMRbpm41 分钟前
链表实现栈:具体函数实现
数据结构·c++·新手入门
Want59543 分钟前
C/C++跳动的爱心③
java·c语言·c++
量子炒饭大师43 分钟前
Cyber骇客的数据链路重构 ——【初阶数据结构与算法】线性表之单链表
c语言·数据结构·c++·windows·git·链表·github
星轨初途1 小时前
C++的条件判断与循环及数组(算法竞赛类)
开发语言·c++·经验分享·笔记·算法
freedom_1024_1 小时前
C++运算符重载:从本质到实践
开发语言·c++
郝学胜-神的一滴1 小时前
Linux信号的概念与机制
linux·服务器·开发语言·c++·程序人生
编程小Y1 小时前
C++ ODB ORM 从入门到实战应用
开发语言·c++
宠..1 小时前
创建标签控件
java·服务器·开发语言·前端·c++·qt