Building a Simple Engine -- Tooling -- Crash minidumps

工具链:崩溃处理与 GPU 崩溃转储

Vulkan 应用中的崩溃处理

即便经过全面的测试与调试,应用在生产环境中仍可能发生崩溃。此时,健壮的崩溃处理机制能帮助你快速诊断并修复问题。本章聚焦于实用的 GPU 崩溃诊断方案(如 NVIDIA Nsight Aftermath、AMD Radeon GPU Detective),同时阐明操作系统进程小型转储文件(minidump)的作用与局限性 ------ 这类文件通常缺失 GPU 状态信息,仅凭其本身很难定位图形渲染 / 设备丢失类问题的根本原因。

理解 Vulkan 应用的崩溃类型

Vulkan 应用崩溃的原因多种多样:

  • API 使用错误(API Usage Errors):调试构建中验证层可捕获的 Vulkan API 误用问题
  • 驱动程序漏洞(Driver Bugs):仅在特定硬件或工作负载下触发的 GPU 驱动问题
  • 资源管理问题(Resource Management Issues):内存泄漏、重复释放、访问已销毁资源等
  • 着色器错误(Shader Errors):导致 GPU 挂起的着色器运行时错误
  • 系统级问题(System-Level Issues):内存不足、系统不稳定等

接下来我们将探讨如何处理这些崩溃并收集诊断信息。

实现基础崩溃处理机制

首先,实现一个基础崩溃处理器,用于捕获未处理的异常和段错误:

cpp 复制代码
import std;
import vulkan_raii;

// 崩溃处理全局状态
namespace crash_handler {
    std::string app_name;
    std::string crash_log_path;
    bool initialized = false;

    // 记录基础系统信息
    void log_system_info(std::ofstream& log) {
        log << "Application: " << app_name << std::endl;
        log << "Timestamp: " << std::chrono::system_clock::now() << std::endl;

        // 记录操作系统信息
        #if defined(_WIN32)
        log << "OS: Windows" << std::endl;
        #elif defined(__linux__)
        log << "OS: Linux" << std::endl;
        #elif defined(__APPLE__)
        log << "OS: macOS" << std::endl;
        #else
        log << "OS: Unknown" << std::endl;
        #endif

        // 记录CPU信息
        log << "CPU Cores: " << std::thread::hardware_concurrency() << std::endl;

        // 记录内存信息
        #if defined(_WIN32)
        MEMORYSTATUSEX mem_info;
        mem_info.dwLength = sizeof(MEMORYSTATUSEX);
        GlobalMemoryStatusEx(&mem_info);
        log << "Total Physical Memory: " << mem_info.ullTotalPhys / (1024 * 1024) << " MB" << std::endl;
        log << "Available Memory: " << mem_info.ullAvailPhys / (1024 * 1024) << " MB" << std::endl;
        #elif defined(__linux__)
        // Linux专属内存信息记录代码
        #elif defined(__APPLE__)
        // macOS专属内存信息记录代码
        #endif
    }

    // 记录Vulkan专属信息
    void log_vulkan_info(std::ofstream& log, vk::raii::PhysicalDevice* physical_device = nullptr) {
        if (physical_device) {
            auto properties = physical_device->getProperties();
            log << "GPU: " << properties.deviceName << std::endl;
            log << "Driver Version: " << properties.driverVersion << std::endl;
            log << "Vulkan API Version: "
                << VK_VERSION_MAJOR(properties.apiVersion) << "."
                << VK_VERSION_MINOR(properties.apiVersion) << "."
                << VK_VERSION_PATCH(properties.apiVersion) << std::endl;
        } else {
            log << "No Vulkan physical device information available" << std::endl;
        }
    }

    // 未处理异常处理器
    void handle_exception(const std::exception& e, vk::raii::PhysicalDevice* physical_device = nullptr) {
        try {
            std::ofstream log(crash_log_path, std::ios::app);
            log << "==== Crash Report ====" << std::endl;
            log_system_info(log);
            log_vulkan_info(log, physical_device);

            log << "Exception: " << e.what() << std::endl;
            log << "==== End of Crash Report ====" << std::endl << std::endl;

            log.close();
        } catch (...) {
            // 若日志写入失败的最后兜底方案
            std::cerr << "Failed to write crash log" << std::endl;
        }
    }

    // 段错误等信号处理器
    void signal_handler(int signal) {
        try {
            std::ofstream log(crash_log_path, std::ios::app);
            log << "==== Crash Report ====" << std::endl;
            log_system_info(log);

            log << "Signal: " << signal << " (";
            switch (signal) {
                case SIGSEGV: log << "SIGSEGV - Segmentation fault"; break;
                case SIGILL: log << "SIGILL - Illegal instruction"; break;
                case SIGFPE: log << "SIGFPE - Floating point exception"; break;
                case SIGABRT: log << "SIGABRT - Abort"; break;
                default: log << "Unknown signal"; break;
            }
            log << ")" << std::endl;

            log << "==== End of Crash Report ====" << std::endl << std::endl;

            log.close();
        } catch (...) {
            // 若日志写入失败的最后兜底方案
            std::cerr << "Failed to write crash log" << std::endl;
        }

        // 重新触发信号,交由默认处理器处理
        signal(signal, SIG_DFL);
        raise(signal);
    }

    // 初始化崩溃处理器
    void initialize(const std::string& application_name, const std::string& log_path) {
        if (initialized) return;

        app_name = application_name;
        crash_log_path = log_path;

        // 设置信号处理器
        signal(SIGSEGV, signal_handler);
        signal(SIGILL, signal_handler);
        signal(SIGFPE, signal_handler);
        signal(SIGABRT, signal_handler);

        initialized = true;
    }
}

// 主应用中的使用示例
int main() {
    try {
        // 初始化崩溃处理器
        crash_handler::initialize("MyVulkanApp", "crash_log.txt");

        // 初始化Vulkan
        vk::raii::Context context;
        auto instance = create_instance(context);
        auto physical_device = select_physical_device(instance);
        auto device = create_device(physical_device);

        // 主应用循环
        while (true) {
            try {
                // 渲染帧
                render_frame(device);
            } catch (const vk::SystemError& e) {
                // 处理可恢复的Vulkan错误
                std::cerr << "Vulkan error: " << e.what() << std::endl;
            }
        }
    } catch (const std::exception& e) {
        // 处理不可恢复的异常
        crash_handler::handle_exception(e);
        return 1;
    }

    return 0;
}

GPU 崩溃诊断(Vulkan)

操作系统进程 minidump 仅能捕获 CPU 端状态,而 GPU 崩溃(设备丢失、TDR、挂起)需要 GPU 专属崩溃转储文件才能定位根本原因。实际开发中,你需要集成厂商工具来记录故障发生时的 GPU 执行状态。

NVIDIA:Nsight Aftermath(Vulkan)

概述:
  • 收集包含最后执行的绘制 / 调度操作、绑定的管线 / 着色器、标记、资源标识符等信息的 GPU 崩溃转储文件
  • 与 Vulkan 应用协同工作;通过 NVIDIA 工具分析转储文件,精确定位出错的工作负载和着色器
实操步骤:
  1. 启用对象命名与标记使用 VK_EXT_debug_utils 为管线、着色器、图像、缓冲区命名,并为主要渲染阶段和绘制 / 调度组插入命令缓冲区标记。这些名称会显示在崩溃报告中,大幅提升问题排查效率。

  2. 添加帧 / 工作负载标记在关键渲染阶段前后插入具备业务含义的标记。若目标平台支持,还可使用厂商检查点 / 标记扩展(如 VK_NV_device_diagnostic_checkpoints),提供细粒度的执行轨迹。

  3. 为着色器构建唯一 ID 并可选添加调试信息确保每个管线 / 着色器可被关联(例如在管线缓存和应用日志中包含稳定哈希 / UUID)。保留 ID 到源代码的映射关系,便于分析。

  4. 初始化并启用 GPU 崩溃转储按照 NVIDIA 文档集成 Nsight Aftermath Vulkan SDK。注册回调函数接收崩溃转储数据,将其写入磁盘,并包含标记字符串表以支持符号化解析。

  5. 处理设备丢失当触发 VK_ERROR_DEVICE_LOST(或 Windows TDR)时,刷新内存中的标记日志,持久化崩溃转储文件,然后干净地终止程序。尝试继续渲染会导致未定义行为。

参考资料:

NVIDIA Nsight Aftermath SDK 及官方文档

AMD:Radeon GPU Detective(RGD)

AMD 提供了针对 RDNA 架构硬件的 GPU 崩溃信息捕获与分析工具。核心原则与 NVIDIA 方案一致:启用对象命名、标记命令缓冲区、保留管线 / 着色器标识符,使 RGD 能关联到对应的代码内容。

请参考 AMD Radeon GPU Detective 及相关文档,了解 Vulkan 集成方式与分析流程。

适用于所有工具的通用基础工作

  • 通过 VK_EXT_debug_utils 为所有对象命名
  • 在具备业务意义的边界(帧、渲染阶段、材质批次等)插入命令缓冲区标记
  • 在日志和崩溃产物中记录构建 / 版本、驱动、Vulkan API/UUID、管线缓存 UUID
  • 实现健壮的设备丢失处理逻辑:停止提交工作、安全释放 / 销毁资源、写入诊断产物、退出程序

生成 Minidump 文件

使用操作系统进程 minidump 捕获崩溃时的 CPU 端调用栈、线程、内存快照。对于图形渲染问题和设备丢失,minidump 几乎不包含所需的 GPU 执行状态 ------ 应将其视为 GPU 崩溃转储的补充,而非替代品。

以下是使用平台 API 生成 minidump 的简要示例(用于关联 GPU 崩溃时的 CPU 上下文):

cpp 复制代码
import std;
import vulkan_raii;

namespace crash_handler {
    std::string app_name;
    std::string dump_path;
    bool initialized = false;

    #if defined(_WIN32)
    // 基于Windows Error Reporting (WER)的实现
    LONG WINAPI windows_exception_handler(EXCEPTION_POINTERS* exception_pointers) {
        // 为minidump创建唯一文件名
        std::string filename = dump_path + "\\" + app_name + "_" +
            std::to_string(std::chrono::system_clock::now().time_since_epoch().count()) + ".dmp";

        // 创建minidump文件
        HANDLE file = CreateFileA(
            filename.c_str(),
            GENERIC_WRITE,
            0,
            nullptr,
            CREATE_ALWAYS,
            FILE_ATTRIBUTE_NORMAL,
            nullptr
        );

        if (file != INVALID_HANDLE_VALUE) {
            // 初始化minidump信息
            MINIDUMP_EXCEPTION_INFORMATION exception_info;
            exception_info.ThreadId = GetCurrentThreadId();
            exception_info.ExceptionPointers = exception_pointers;
            exception_info.ClientPointers = FALSE;

            // 写入minidump
            MiniDumpWriteDump(
                GetCurrentProcess(),
                GetCurrentProcessId(),
                file,
                MiniDumpWithFullMemory,  // 转储类型
                &exception_info,
                nullptr,
                nullptr
            );

            CloseHandle(file);

            std::cerr << "Minidump written to: " << filename << std::endl;
        } else {
            std::cerr << "Failed to create minidump file" << std::endl;
        }

        // 继续执行常规异常处理
        return EXCEPTION_CONTINUE_SEARCH;
    }

    void initialize(const std::string& application_name, const std::string& minidump_path) {
        if (initialized) return;

        app_name = application_name;
        dump_path = minidump_path;

        // 若目录不存在则创建
        CreateDirectoryA(dump_path.c_str(), nullptr);

        // 设置异常处理器
        SetUnhandledExceptionFilter(windows_exception_handler);

        initialized = true;
    }

    #elif defined(__linux__)
    // 基于Google Breakpad的Linux实现
    // 注意:需链接Google Breakpad库

    #include "client/linux/handler/exception_handler.h"

    // minidump生成回调函数
    static bool minidump_callback(const google_breakpad::MinidumpDescriptor& descriptor,
                                 void* context, bool succeeded) {
        std::cerr << "Minidump generated: " << descriptor.path() << std::endl;
        return succeeded;
    }

    google_breakpad::ExceptionHandler* exception_handler = nullptr;

    void initialize(const std::string& application_name, const std::string& minidump_path) {
        if (initialized) return;

        app_name = application_name;
        dump_path = minidump_path;

        // 若目录不存在则创建
        std::filesystem::create_directories(dump_path);

        // 设置异常处理器
        google_breakpad::MinidumpDescriptor descriptor(dump_path);
        exception_handler = new google_breakpad::ExceptionHandler(
            descriptor,
            nullptr,
            minidump_callback,
            nullptr,
            true,
            -1
        );

        initialized = true;
    }

    #elif defined(__APPLE__)
    // 基于Google Breakpad的macOS实现
    // 与Linux实现类似
    #endif
}

分析 Minidump 文件

Minidump 最适合用于分析崩溃时的 CPU 端状态(例如哪个线程出错、导致 vkQueueSubmit/vkQueuePresent 的调用栈、分配器误用等),并与厂商工具生成的 GPU 崩溃转储进行关联分析。以下是不同平台的简要分析流程:

Windows

在 Windows 平台,可使用 Visual Studio 或 WinDbg 分析 minidump:

Visual Studio:
  1. 打开 Visual Studio
  2. 依次点击 File > Open > File,选择.dmp 文件
  3. Visual Studio 会加载 minidump 并显示崩溃时的调用栈
WinDbg:
  1. 打开 WinDbg
  2. 打开 minidump 文件
  3. 使用.ecxr 命令检查异常上下文记录
  4. 使用 k 命令查看调用栈

Linux 和 macOS

在 Linux 和 macOS 平台,可使用 GDB 或 LLDB 分析 Google Breakpad 生成的 minidump:

  • 使用 minidump_stackwalk(Google Breakpad 组件):

    bash 复制代码
    minidump_stackwalk minidump_file.dmp /path/to/symbols > stacktrace.txt
  • 使用 GDB:

    cpp 复制代码
    gdb /path/to/executable
    (gdb) core-file /path/to/minidump
    (gdb) bt

Vulkan 专属崩溃信息

对于 Vulkan 应用,在崩溃报告中包含以下额外信息会更有帮助:

cpp 复制代码
void log_vulkan_detailed_info(std::ofstream& log, vk::raii::PhysicalDevice& physical_device,
                             vk::raii::Device& device) {
    // 记录物理设备属性
    auto properties = physical_device.getProperties();
    log << "GPU: " << properties.deviceName << std::endl;
    log << "Driver Version: " << properties.driverVersion << std::endl;
    log << "Vulkan API Version: "
        << VK_VERSION_MAJOR(properties.apiVersion) << "."
        << VK_VERSION_MINOR(properties.apiVersion) << "."
        << VK_VERSION_PATCH(properties.apiVersion) << std::endl;

    // 记录内存使用情况
    auto memory_properties = physical_device.getMemoryProperties();
    log << "Memory Heaps:" << std::endl;
    for (uint32_t i = 0; i < memory_properties.memoryHeapCount; i++) {
        log << "  Heap " << i << ": "
            << (memory_properties.memoryHeaps[i].size / (1024 * 1024)) << " MB";
        if (memory_properties.memoryHeaps[i].flags & vk::MemoryHeapFlagBits::eDeviceLocal) {
            log << " (Device Local)";
        }
        log << std::endl;
    }

    // 记录已启用的扩展
    auto extensions = device.enumerateDeviceExtensionProperties();
    log << "Enabled Extensions:" << std::endl;
    for (const auto& ext : extensions) {
        log << "  " << ext.extensionName << " (version " << ext.specVersion << ")" << std::endl;
    }

    // 记录当前管线缓存状态
    // 这对诊断着色器相关崩溃很有帮助
    try {
        auto pipeline_cache_data = device.getPipelineCacheData();
        log << "Pipeline Cache Size: " << pipeline_cache_data.size() << " bytes" << std::endl;
    } catch (const vk::SystemError& e) {
        log << "Failed to get pipeline cache data: " << e.what() << std::endl;
    }
}

与遥测系统集成

对于生产环境应用,你可能希望自动将崩溃报告上传至遥测系统进行分析:

cpp 复制代码
import std;
import vulkan_raii;
#include <curl/curl.h>

namespace crash_handler {
    // ... 现有代码 ...

    std::string telemetry_url;
    bool telemetry_enabled = false;

    // 将minidump上传至遥测服务器
    bool upload_minidump(const std::string& minidump_path) {
        if (!telemetry_enabled || telemetry_url.empty()) {
            return false;
        }

        CURL* curl = curl_easy_init();
        if (!curl) {
            std::cerr << "Failed to initialize curl" << std::endl;
            return false;
        }

        // 设置表单数据
        curl_mime* form = curl_mime_init(curl);

        // 添加minidump文件
        curl_mimepart* field = curl_mime_addpart(form);
        curl_mime_name(field, "minidump");
        curl_mime_filedata(field, minidump_path.c_str());

        // 添加应用信息
        field = curl_mime_addpart(form);
        curl_mime_name(field, "product");
        curl_mime_data(field, app_name.c_str(), CURL_ZERO_TERMINATED);

        // 添加版本信息
        field = curl_mime_addpart(form);
        curl_mime_name(field, "version");
        curl_mime_data(field, "1.0.0", CURL_ZERO_TERMINATED);  // 替换为实际版本号

        // 设置请求参数
        curl_easy_setopt(curl, CURLOPT_URL, telemetry_url.c_str());
        curl_easy_setopt(curl, CURLOPT_MIMEPOST, form);

        // 执行请求
        CURLcode res = curl_easy_perform(curl);

        // 清理资源
        curl_mime_free(form);
        curl_easy_cleanup(curl);

        if (res != CURLE_OK) {
            std::cerr << "Failed to upload minidump: " << curl_easy_strerror(res) << std::endl;
            return false;
        }

        return true;
    }

    // 启用遥测功能
    void enable_telemetry(const std::string& url) {
        telemetry_url = url;
        telemetry_enabled = true;

        // 初始化curl
        curl_global_init(CURL_GLOBAL_ALL);
    }

    // 禁用遥测功能
    void disable_telemetry() {
        telemetry_enabled = false;

        // 清理curl资源
        curl_global_cleanup();
    }
}

崩溃处理最佳实践(聚焦 Vulkan/GPU)

为使崩溃数据能有效定位图形渲染问题,建议遵循以下具体步骤:

充分命名与标记

使用 VK_EXT_debug_utils 为所有对象命名,并在渲染阶段 / 材质边界、大型绘制 / 调度批次前插入命令缓冲区标记。维护一个小型内存环形缓冲区记录近期标记,以便纳入崩溃产物中。

为设备丢失做好准备

实现 VK_ERROR_DEVICE_LOST 的集中处理逻辑:停止提交工作负载、刷新日志 / 标记、请求厂商 GPU 崩溃转储数据、退出程序。除非具备健壮的重新初始化流程,否则避免在同一进程中尝试恢复。

在支持的硬件上捕获 GPU 崩溃转储

根据目标用户群体集成 NVIDIA Nsight Aftermath 和 / 或 AMD RGD。在开发 / 测试版本中默认启用崩溃转储;为正式版用户提供开关选项。

构建符号友好的版本

保留管线 / 着色器哈希到源代码 / IR/SPIR-V 及构建 ID 的映射关系。在诊断版本中尽可能启用着色器调试信息。

记录环境信息

记录驱动版本、Vulkan 版本、GPU 名称 / PCI ID、管线缓存 UUID、应用构建 / 版本、相关功能开关。将这些信息与 minidump 和 GPU 崩溃转储一并保存。

确定性复现问题

提供禁用后台可变因素(如异步流加载)的方式,并支持重放捕获的命令 / 场景序列,以便在本地复现崩溃。

尊重隐私与分发规范

清晰说明收集的崩溃数据类型(minidump、GPU 崩溃转储、日志),上传前需获得用户明确授权。移除用户可识别信息。

总结

健壮的崩溃处理机制是维护高质量 Vulkan 应用的关键。将厂商 GPU 崩溃转储(Aftermath、RGD 等)与 CPU 端 minidump、全面的日志记录相结合,可快速诊断并修复生产环境中的问题。将 minidump 视为补充上下文;图形渲染故障的关键诊断信息通常来自 GPU 崩溃转储工具。

下一节中,我们将探讨用于提升健壮性的 Vulkan 扩展,这些扩展可减少未定义行为,从根源上预防崩溃的发生。

关键点回顾

  1. 操作系统 minidump 仅能捕获 CPU 端状态,GPU 崩溃需依赖 NVIDIA Nsight Aftermath/AMD RGD 等厂商工具获取关键的 GPU 执行状态;
  2. 崩溃处理核心实践包括:为 Vulkan 对象命名标记、实现设备丢失处理、捕获 GPU 崩溃转储、记录完整环境信息;
  3. Minidump 应作为 GPU 崩溃转储的补充,而非替代品,二者结合才能完整定位图形应用崩溃的根本原因。
相关推荐
千里马-horse1 天前
Building a Simple Engine -- Tooling -- Introduction
pipeline·shader·rendering·vulkan
千里马-horse21 天前
Ray Tracing -- Ray query shadows
c++·rendering·vulkan
千里马-horse22 天前
Multithreading with Vulkan
shader·rendering·vulkan·vertex·multithreaded
UWA1 个月前
如何使Bloom只局部地作用于特效以提高性能
memory·rendering
千里马-horse1 个月前
Drawing a triangle -- setup -- Instance
vulkan
千里马-horse1 个月前
Drawing a triangle -- setup -- Validation layers
validation·vulkan·layers
UWA2 个月前
哪些因素和参数会影响Bloom的性能开销
性能优化·script·rendering
李坤林2 个月前
Android Vulkan 开启VK_GOOGLE_DISPLAY_TIMING 后,一个vsync 会释放两个imageBuffer现象分析
android·vulkan
不知所云,3 个月前
1. 开篇简介
c++·vulkan