生产环境内存溢出如何调试

0 阅读4分钟

目的

生产环境遇上崩溃的问题怎么办?这真是一个令人头疼的问题,记得我以前遇上这个问题,全靠日志进行推测,那样效率真是太慢了。 现在说的是在windows环境上如何调试,下面一步步,说说这个过程: 在这里插入图片描述

过程

代码方面需要生成崩溃文件

崩溃文件记录崩溃的详情,这是最为关键的一步,代码如下:

// main.cpp
#include <QCoreApplication>
#include <QVector>
#include <QDebug>
#include <QDir>
#include <QDateTime>
#include <windows.h>
#include <dbghelp.h>
#include <tchar.h>
#pragma comment(lib, "dbghelp.lib")
// ============================================================
// 生成 Dump 文件的函数
// ============================================================
void WriteDumpFile(EXCEPTION_POINTERS* pExceptionInfo)
{
    // Dump 文件保存到程序所在目录,文件名带时间戳
    QString dumpDir = QCoreApplication::applicationDirPath();
    QString dumpName = QString("crash_%1.dmp")
        .arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"));
    QString dumpPath = QDir(dumpDir).filePath(dumpName);
    // 转换为宽字符路径(MiniDumpWriteDump 需要 wchar_t*)
    std::wstring wDumpPath = dumpPath.toStdWString();
    // 创建 Dump 文件
    HANDLE hFile = CreateFileW(
        wDumpPath.c_str(),
        GENERIC_WRITE,
        0,
        NULL,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);
    if (hFile == INVALID_HANDLE_VALUE)
    {
        qDebug() << "CreateFile failed, error:" << GetLastError();
        return;
    }
    // Dump 类型:包含完整内存、句柄、线程、未加载模块等
    // 越详细越好,方便离线分析
    MINIDUMP_TYPE dumpType = (MINIDUMP_TYPE)(
        MiniDumpWithFullMemory |   // 完整内存
        MiniDumpWithFullMemoryInfo |   // 内存区域信息
        MiniDumpWithHandleData |   // 句柄信息
        MiniDumpWithThreadInfo |   // 线程信息
        MiniDumpWithUnloadedModules |   // 已卸载模块
        MiniDumpWithProcessThreadData |   // 进程/线程数据
        MiniDumpWithFullAuxiliaryState |   // 辅助状态
        MiniDumpIgnoreInaccessibleMemory      // 忽略不可访问内存,避免二次崩溃
        );
    MINIDUMP_EXCEPTION_INFORMATION dumpExceptionInfo;
    dumpExceptionInfo.ThreadId = GetCurrentThreadId();
    dumpExceptionInfo.ExceptionPointers = pExceptionInfo;
    dumpExceptionInfo.ClientPointers = FALSE;
    BOOL bOK = MiniDumpWriteDump(
        GetCurrentProcess(),                 // 目标进程
        GetCurrentProcessId(),               // 进程ID
        hFile,                               // 输出文件句柄
        dumpType,                            // Dump 类型
        pExceptionInfo ? &dumpExceptionInfo : NULL,  // 异常信息
        NULL,                                // 用户自定义流
        NULL);                               // 回调
    CloseHandle(hFile);
    if (bOK)
    {
        qDebug() << "Dump file saved to:" << dumpPath;
        // 在控制台也输出一份,方便定位
        OutputDebugStringW((L"Dump saved: " + wDumpPath + L"\n").c_str());
    }
    else
    {
        qDebug() << "MiniDumpWriteDump failed, error:" << GetLastError();
    }
}
// ============================================================
// 未处理异常过滤器:崩溃时先写 Dump,再让系统处理
// ============================================================
LONG WINAPI MyUnhandledExceptionFilter(EXCEPTION_POINTERS* pExceptionInfo)
{
    qDebug() << "========== Unhandled exception caught ==========";
    qDebug() << "Exception code: 0x" << QString::number(
        pExceptionInfo->ExceptionRecord->ExceptionCode, 16);
    qDebug() << "Exception address: 0x" << QString::number(
        (quintptr)pExceptionInfo->ExceptionRecord->ExceptionAddress, 16);
    // 写 Dump
    WriteDumpFile(pExceptionInfo);
    // 返回 EXCEPTION_EXECUTE_HANDLER 表示已处理
    // 这里返回 EXCEPTION_CONTINUE_SEARCH 让程序继续崩溃(便于调试器捕获)
    // 生产环境中可以返回 EXCEPTION_EXECUTE_HANDLER 优雅退出
    return EXCEPTION_EXECUTE_HANDLER;
}
// ============================================================
// 崩溃复现函数:QVector 越界写入,破坏堆
// ============================================================
void TriggerHeapCorruption()
{
    qDebug() << "Creating QVector with reserve(10)...";
    QVector<int> vec;
    vec.reserve(10);
    qDebug() << "size:" << vec.size()
        << "capacity:" << vec.capacity();
    // 关键:越界写入,超出 capacity 边界,破坏相邻堆内存
    // Release 模式下 operator[] 不做检查
    qDebug() << "Writing out of bounds at index 100000 and beyond...";
    // 故意越界写入:索引 10 超出 [0,9],且超出 capacity 边界
    vec.data()[100000] = 1;
    
    qDebug() << "Out-of-bounds write done.";
    // 函数结束时 vec 析构 -> free -> 堆检查失败 -> 触发 0xC0000374
}
// ============================================================
// main
// ============================================================
int main(int argc, char* argv[])
{
    QCoreApplication a(argc, argv);
    // 安装未处理异常过滤器
    SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
    qDebug() << "Program started. PID:" << GetCurrentProcessId();
    qDebug() << "Will trigger heap corruption...";
    TriggerHeapCorruption();
    qDebug() << "If you see this, no crash happened this run.";
    return a.exec();
}

看看上面代码可知,就是把代码的异常信息,当时堆栈信息打印到一个文件里。

项目进行必须的设置

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

运行程序---程序崩溃

文件列表如下: 在这里插入图片描述

开始执行: 在这里插入图片描述

这个程序崩溃了,生成了dmp文件,上面可以看出: 1、c0000005,说明是内存溢出的错误,这是给我们宝贵的信息 2、0x000b2062是指崩溃的代码地址,这个地址很关键,之后找到具体那行代码出了错,全靠这个地址找啊,非常重要的地址。 虽然没有深入研究过pdb文件,但可以推知,pdb文件里就记录着代码地址与代码行的对应关系。

调试程序

在分析之前,需要确保手头有以下文件,并将它们放在同一个目录下,方便后续加载 文件类型 作用 缺失后的影响 Dump文件 (.dmp) 崩溃瞬间的内存快照 无法分析 PDB符号文件 (.pdb) 函数名、变量名、代码行号 只能看到地址,无法看到函数名和源码行 可执行文件 (.exe) 主程序二进制代码 无法反汇编主程序,堆栈不完整 依赖的DLL文件 Qt5Core.dll等依赖库 依赖库调用栈显示为地址 关键要求:exe/dll 和对应的 pdb 文件版本必须完全匹配(时间戳一致)。建议将上述所有文件放在同一个目录中,例如 D:\Crash_Analysis\。 用WinDbg打开崩溃文件: 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 从上面可知,加载符号后,用!analyze -v命令就很快找到问题原因,并定位到了代码行: 在这里插入图片描述

总结

感觉非常得简单,一下子就找到问题了。 很关键的是几个文件,内存快照文件,pdb文件,如图所示: 在这里插入图片描述

从上面可知,生成程序时,必须得生成pdb文件,并且放程序放到一块,否则,如何调试崩溃的程序是一个难题。 一句话总结‌:异常地址是 CPU 执行到的‌机器指令位置‌,通过编译器预先埋下的‌调试映射表‌,工具将这个物理地址“反查”回它所属的‌源代码文件及行号: 在这里插入图片描述