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
??:?

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐