QT Linux系统后台程序崩溃输出到日志文件
CrashLog.cppmain.cpp程序输出日志文件示例=====================================崩溃时间:2026-01-23 14:33:48信号类型:SIGSEGV (段错误/空指针)(PID:102842)程序路径:/home/pwrp/tool/pwrsrc/ReportConfig/bin/ReportConfig加载基址:0x555d1711000
CrashLog.h
#ifndef CRASHLOG_H
#define CRASHLOG_H
#include <QObject>
#include <QFileInfo>
#include <QDateTime>
#define __USE_GNU // 启用GNU扩展,暴露Elf64_Ehdr/Elf64_Phdr等结构体
#include <elf.h>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <execinfo.h>
#include <errno.h>
// ========== 配置项(根据你的环境调整) ==========
#define CRASH_LOG_PATH "/tmp/ReportConfig_crash.log" // 无需root权限,避免权限问题
#define ADDR2LINE_PATH "/usr/bin/addr2line" // addr2line路径(which addr2line 验证)
#define MAPS_BUF_SIZE 16384 // 增大maps缓冲区,避免截断
class CrashLog : public QObject
{
Q_OBJECT
public:
explicit CrashLog(QObject *parent = nullptr);
// ========== 3. 修正initProgramInfo函数中的基址逻辑 ==========
void initProgramInfo();
// ========== 辅助函数:注册崩溃信号 ==========
void registerCrashHandler();
// ========== 核心:崩溃信号处理函数 ==========
static void crashSignalHandler(int sig);
private:
// ========== 2. 修正后的ELF.text段基址解析函数 ==========
static uint64_t getElfTextBase(const char* exePath);
// ========== 辅助函数2:调用addr2line解析地址(纯POSIX,信号安全) ==========
static void resolveCrashAddr(void* addr, int logFd);
private:
static uint64_t g_programBaseAddr;
static char g_realExePath[1024];
signals:
public slots:
};
#endif // CRASHLOG_H
CrashLog.cpp
#include "crashlog.h"
// 初始化静态成员变量
uint64_t CrashLog::g_programBaseAddr = 0;
char CrashLog::g_realExePath[1024] = {0};
CrashLog::CrashLog(QObject *parent) : QObject(parent)
{
}
uint64_t CrashLog::getElfTextBase(const char *exePath) {
if (exePath == nullptr || strlen(exePath) == 0) {
fprintf(stderr, "解析ELF失败:程序路径为空\n");
return 0;
}
// 打开ELF文件(只读模式)
int fd = open(exePath, O_RDONLY);
if (fd < 0) {
fprintf(stderr, "解析ELF失败:打开文件%s失败(%s)\n", exePath, strerror(errno));
return 0;
}
// 读取ELF文件头(64位)
Elf64_Ehdr ehdr;
ssize_t readLen = read(fd, &ehdr, sizeof(Elf64_Ehdr));
if (readLen != sizeof(Elf64_Ehdr)) {
fprintf(stderr, "解析ELF失败:读取文件头失败(读取长度:%zd)\n", readLen);
close(fd);
return 0;
}
// 验证是否为合法ELF文件(ELF魔数:0x7f 0x45 0x4c 0x46)
if (memcmp(ehdr.e_ident, ELFMAG, 4) != 0) {
fprintf(stderr, "解析ELF失败:不是合法的ELF文件\n");
close(fd);
return 0;
}
// 验证是否为64位ELF(避免32位/64位不匹配)
if (ehdr.e_ident[EI_CLASS] != ELFCLASS64) {
fprintf(stderr, "解析ELF失败:仅支持64位ELF文件\n");
close(fd);
return 0;
}
// 读取程序头表,找到可执行的LOAD段(.text段)
Elf64_Phdr phdr;
lseek(fd, ehdr.e_phoff, SEEK_SET); // 定位到程序头表起始位置
for (int i = 0; i < ehdr.e_phnum; i++) {
readLen = read(fd, &phdr, sizeof(Elf64_Phdr));
if (readLen != sizeof(Elf64_Phdr)) {
fprintf(stderr, "解析ELF失败:读取程序头%d失败\n", i);
close(fd);
return 0;
}
// 匹配条件:LOAD段 + 可执行(PF_X) + 可读(PF_R)
if (phdr.p_type == PT_LOAD && (phdr.p_flags & PF_X) && (phdr.p_flags & PF_R)) {
close(fd);
return phdr.p_vaddr; // 返回ELF文件内.text段的虚拟基址(如0x4000)
}
}
fprintf(stderr, "解析ELF失败:未找到可执行的LOAD段(.text)\n");
close(fd);
return 0;
}
void CrashLog::initProgramInfo() {
// 1. 获取程序真实路径(原逻辑不变)
ssize_t exePathLen = readlink("/proc/self/exe", g_realExePath, sizeof(g_realExePath) - 1);
if (exePathLen <= 0) {
fprintf(stderr, "获取程序路径失败:%s\n", strerror(errno));
return;
}
fprintf(stdout, "程序真实路径:%s\n", g_realExePath);
// 2. 读取/proc/self/maps解析进程加载基址(原逻辑不变)
int mapsFd = open("/proc/self/maps", O_RDONLY);
if (mapsFd < 0) {
fprintf(stderr, "打开maps失败:%s\n", strerror(errno));
return;
}
char buf[MAPS_BUF_SIZE] = {0};
ssize_t readLen = read(mapsFd, buf, sizeof(buf) - 1);
close(mapsFd);
if (readLen <= 0) {
fprintf(stderr, "读取maps失败:%s\n", strerror(errno));
return;
}
char* line = strtok(buf, "\n");
while (line != nullptr) {
if (strstr(line, "r-xp") && strstr(line, g_realExePath) && !strstr(line, ".so")) {
if (sscanf(line, "%lx-", &g_programBaseAddr) == 1) {
fprintf(stdout, "进程加载基址(未修正):0x%lx\n", g_programBaseAddr);
break;
}
}
line = strtok(nullptr, "\n");
}
if (g_programBaseAddr == 0) {
fprintf(stderr, "警告:未找到进程加载基址!\n");
return;
}
// 3. 解析ELF文件内.text段的基址,修正进程基址
uint64_t elfTextBase = getElfTextBase(g_realExePath);
if (elfTextBase > 0) {
g_programBaseAddr = g_programBaseAddr - elfTextBase; // 核心修正:减ELF.text基址
fprintf(stdout, "ELF.text段内部基址:0x%lx\n", elfTextBase);
fprintf(stdout, "修正后最终基址:0x%lx\n", g_programBaseAddr);
} else {
fprintf(stderr, "警告:ELF解析失败,使用硬编码修正(-0x4000)\n");
g_programBaseAddr = g_programBaseAddr - 0x4000; // 兜底方案
}
}
void CrashLog::resolveCrashAddr(void *addr, int logFd) {
if (!addr) {
dprintf(logFd, " 解析失败:地址为空\n");
return;
}
if (g_programBaseAddr == 0) {
dprintf(logFd, " 解析失败:基址未初始化\n");
dprintf(logFd, " 提示:程序路径=%s,maps匹配是否成功?\n", g_realExePath);
return;
}
// 关键优化1:返回地址减1(适配backtrace的指令指针偏移)
uint64_t virtAddr = reinterpret_cast<uint64_t>(addr);
// 过滤系统库地址(0x7f开头)
if (virtAddr >= 0x700000000000) {
dprintf(logFd, " 跳过系统库地址解析(无调试信息)\n");
return;
}
uint64_t offsetAddr = virtAddr - g_programBaseAddr;
dprintf(logFd, " 虚拟地址:0x%lx → 偏移地址:0x%lx\n", virtAddr, offsetAddr);
// 关键优化2:fork子进程调用addr2line(增强错误日志)
pid_t pid = fork();
if (pid == -1) {
dprintf(logFd, " 解析失败:fork失败(错误码:%d,%s)\n", errno, strerror(errno));
return;
}
if (pid == 0) { // 子进程:执行addr2line
// 重定向stdout/stderr到日志(确保输出被捕获)
dup2(logFd, STDOUT_FILENO);
dup2(logFd, STDERR_FILENO);
// 格式化偏移地址(纯十六进制,不带0x)
char addrStr[32] = {0};
snprintf(addrStr, sizeof(addrStr), "%lx", offsetAddr);
// 构造addr2line参数(明确指定路径,避免环境变量问题)
const char* argv[] = {
ADDR2LINE_PATH,
"-f", "-C", "-i", "-s", // -s:简化路径(可选)
"-e", g_realExePath, // 使用预存的真实程序路径
addrStr,
nullptr
};
// 执行addr2line(失败则打印详细错误)
execvp(argv[0], (char* const*)argv);
dprintf(logFd, " addr2line执行失败:\n");
dprintf(logFd, " 路径:%s\n", ADDR2LINE_PATH);
dprintf(logFd, " 程序:%s\n", g_realExePath);
dprintf(logFd, " 地址:%s\n", addrStr);
dprintf(logFd, " 错误:%d → %s\n", errno, strerror(errno));
_exit(1); // 子进程强制退出
} else { // 父进程:等待子进程结束(避免僵尸进程)
int status;
waitpid(pid, &status, 0);
if (WEXITSTATUS(status) != 0) {
dprintf(logFd, " 警告:addr2line子进程退出码非0(%d)\n", WEXITSTATUS(status));
}
}
}
void CrashLog::crashSignalHandler(int sig) {
int logFd = open(CRASH_LOG_PATH, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (logFd >= 0) { // 仅当文件打开成功时,执行日志写入
char timeBuf[64] = {0};
time_t now = time(nullptr);
strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M:%S", localtime(&now));
const char* sigName = nullptr;
switch (sig) {
case SIGSEGV: sigName = "SIGSEGV (段错误/空指针)"; break;
case SIGABRT: sigName = "SIGABRT (程序中止/断言失败)"; break;
case SIGFPE: sigName = "SIGFPE (浮点异常)"; break;
case SIGILL: sigName = "SIGILL (非法指令)"; break;
default: sigName = "未知信号"; break;
}
dprintf(logFd, "\n=====================================\n");
dprintf(logFd, "崩溃时间:%s\n", timeBuf);
dprintf(logFd, "信号类型:%s(PID:%d)\n", sigName, getpid());
dprintf(logFd, "程序路径:%s\n", g_realExePath);
dprintf(logFd, "加载基址:0x%lx\n", g_programBaseAddr);
dprintf(logFd, "崩溃调用栈(函数+行号):\n");
void* callstack[64] = {0};
int frameCount = backtrace(callstack, 64);
for (int i = 0; i < frameCount; i++) {
dprintf(logFd, "\n第%d层栈帧:\n", i);
resolveCrashAddr(callstack[i], logFd);
}
dprintf(logFd, "=====================================\n");
close(logFd);
} else {
fprintf(stderr, "打开崩溃日志失败:%s\n", strerror(errno));
}
// 恢复默认信号处理并退出(无goto,逻辑更清晰)
signal(sig, SIG_DFL);
raise(sig);
}
void CrashLog::registerCrashHandler() {
// 注册核心崩溃信号(增加SIGBUS总线错误)
signal(SIGSEGV, crashSignalHandler);
signal(SIGABRT, crashSignalHandler);
signal(SIGFPE, crashSignalHandler);
signal(SIGILL, crashSignalHandler);
signal(SIGBUS, crashSignalHandler);
// 屏蔽信号嵌套(可选,避免递归崩溃)
sigset_t sigSet;
sigemptyset(&sigSet);
sigaddset(&sigSet, SIGSEGV);
sigaddset(&sigSet, SIGABRT);
sigaddset(&sigSet, SIGFPE);
sigaddset(&sigSet, SIGILL);
sigaddset(&sigSet, SIGBUS);
sigprocmask(SIG_UNBLOCK, &sigSet, nullptr);
}
main.cpp
#include <QGuiApplication>
#include <QTimer>
#include <crashlog.h>
int main(int argc, char *argv[])
{
QGuiApplication a(argc, argv);
//程序崩溃日志输出类
CrashLog *craLog = new CrashLog();
craLog->initProgramInfo();
craLog->registerCrashHandler();
//模拟程序崩溃
int*b = NULL;
*b = 10;
int ret = a.exec();
return ret;
}
pro文件中加上配置
核心:保留调试符号,确保地址能解析到行号
QMAKE_CXXFLAGS += -g3 -rdynamic # 完整调试信息 + 导出所有符号
QMAKE_LFLAGS += -g3 -rdynamic
CONFIG -= strip # 禁止剥离符号(关键!)
QMAKE_LFLAGS -= -s # 禁用链接时的符号剥离
程序输出日志文件示例
=====================================
崩溃时间:2026-01-23 14:33:48
信号类型:SIGSEGV (段错误/空指针)(PID:102842)
程序路径:/home/pwrp/tool/pwrsrc/ReportConfig/bin/ReportConfig
加载基址:0x555d17110000
崩溃调用栈(函数+行号):
第0层栈帧:
虚拟地址:0x555d173a7164 → 偏移地址:0x297164
CrashLog::crashSignalHandler(int)
crashlog.cpp:207
第1层栈帧:
跳过系统库地址解析(无调试信息)
第2层栈帧:
虚拟地址:0x555d173a19f9 → 偏移地址:0x2919f9
main
main.cpp:17
第3层栈帧:
跳过系统库地址解析(无调试信息)
第4层栈帧:
虚拟地址:0x555d173a18be → 偏移地址:0x2918be
_start
??:?
更多推荐



所有评论(0)