在进程外调用时 MiniDumpWriteDump 中的访问冲突

Access violation in MiniDumpWriteDump when invoked out-of-process

The documentation of the MiniDumpWriteDump function 表示

MiniDumpWriteDump should be called from a separate process if at all possible, rather than from within the target process being dumped.

所以我编写了一个小型 MFC 崩溃处理程序来执行此操作。我遵循了 Hans Passant 在 this SO answer 中的建议,即我将异常指针的值从崩溃程序传递到崩溃处理程序,即使异常指针在崩溃处理程序的上下文中无效。当我 运行 在调试版本中进行测试时,这很有效,但是当我切换到发布版本时,崩溃处理程序崩溃,并在 MiniDumpWriteDump 函数内部发生访问冲突。

我很难过。为什么这应该在调试版本中工作,而不是在发布版本中?这令人抓狂,因为访问冲突通常是访问无效指针的指标,而我在崩溃处理程序中收到的异常指针确实无效——但另一方面我被告知这无关紧要,MiniDumpWriteDump 是在崩溃进程的上下文中解释指针(从指针起源的地方)。

知道我可能做错了什么吗?

旁注:在他的回答中,Hans 提出了一个解决方案,其中 watchdog 进程是预启动的,然后进入休眠状态,并在崩溃进程触发时唤醒。我的解决方案略有不同:我只是在崩溃发生时启动崩溃处理程序,然后通过命令行参数将崩溃程序的必要信息传递给崩溃处理程序。我仔细检查了传递的信息是否正确,特别是异常指针。

我曾遇到过类似的问题,现在终于发现问题出在哪里了。

MINIDUMP_EXCEPTION_INFORMATION 的 MSDN 文档指出,如果 ExceptionPointers 地址来自目标进程而不是本地进程,则 ClientPointers 字段必须是 TRUE

正确设置这个字段后,我可以简单地从崩溃过程中传递ThreadIdExceptionPointers,在转储写入过程中将它们填充到MINIDUMP_EXCEPTION_INFORMATION中,并且它有效完美。

我改变了我的方法,使最终解决方案看起来像 Hans Passant 建议的那样:看门狗进程是预启动的,然后进入休眠状态,并在崩溃进程触发时唤醒。崩溃进程对 EXCEPTION_POINTERS 结构进行深度复制,并将该信息传递给看门狗进程。

这是进行深拷贝的代码。正如问题评论中提到的,主要的 "problem" 是 EXCEPTION_RECORD,它是一个可能无限大小的链表。

// The maximum number of nested exception that we can handle. The value we
// use for this constant is an arbitrarily chosen number that is, hopefully,
// sufficiently high to support all realistic and surrealistic scenarios.
//
// sizeof(CrashInfo) for a maximum of 1000 = ca. 80 KB
const int MaximumNumberOfNestedExceptions = 1000;


// Structure with information about the crash that we can pass to the
// watchdog process
struct CrashInfo
{
    EXCEPTION_POINTERS exceptionPointers;
    int numberOfExceptionRecords;
    // Contiguous area of memory that can easily be processed by memcpy
    EXCEPTION_RECORD exceptionRecords[MaximumNumberOfNestedExceptions];
    CONTEXT contextRecord;
};


// The EXCEPTION_POINTERS parameter is the original exception pointer
// that we are going to deep-copy.
// The CrashInfo parameter receives the copy.
void FillCrashInfoWithExceptionPointers(CrashInfo& crashInfo, EXCEPTION_POINTERS* exceptionPointers)
{
    // De-referencing creates a copy
    crashInfo.exceptionPointers = *exceptionPointers;
    crashInfo.contextRecord = *(exceptionPointers->ContextRecord);

    int indexOfExceptionRecord = 0;
    crashInfo.numberOfExceptionRecords = 0;
    EXCEPTION_RECORD* exceptionRecord = exceptionPointers->ExceptionRecord;

    while (exceptionRecord != 0)
    {
        if (indexOfExceptionRecord >= MaximumNumberOfNestedExceptions)
        {
            // Yikes, maximum number of nested exceptions reached
            break;
        }

        // De-referencing creates a copy
        crashInfo.exceptionRecords[indexOfExceptionRecord] = *exceptionRecord;

        ++indexOfExceptionRecord;
        ++crashInfo.numberOfExceptionRecords;
        exceptionRecord = exceptionRecord->ExceptionRecord;
    }
}

当我们在看门狗进程中收到 CrashInfo 结构时,我们现在遇到了一个问题:EXCEPTION_RECORD 引用指向无效的内存地址,即指向仅在崩溃时有效的内存地址过程。以下函数(在看门狗进程中必须是 运行)修复了这些引用。

// The CrashInfo parameter is both in/out
void FixExceptionPointersInCrashInfo(CrashInfo& crashInfo)
{
    crashInfo.exceptionPointers.ContextRecord = &(crashInfo.contextRecord);

    for (int indexOfExceptionRecord = 0; indexOfExceptionRecord < crashInfo.numberOfExceptionRecords; ++indexOfExceptionRecord)
    {
        if (0 == indexOfExceptionRecord)
            crashInfo.exceptionPointers.ExceptionRecord = &(crashInfo.exceptionRecords[indexOfExceptionRecord]);
        else
            crashInfo.exceptionRecords[indexOfExceptionRecord - 1].ExceptionRecord = &(crashInfo.exceptionRecords[indexOfExceptionRecord]);
    }
}

我们现在准备将 &(crashInfo.exceptionPointers) 传递给 MiniDumpWriteDump 函数。

注意:显然这不是一个完整的解决方案。您可能希望将更多信息从崩溃进程传递到看门狗进程。 CrashInfo 结构是保存此信息的候选者。此外,此处未显示进程如何相互通信的方式。就我而言,我采用了 Hans Passant 提出的解决方案,该解决方案链接在问题的开头:Use an event for synchronization (CreateEvent + SetEvent) and a memory-mapped file (CreateFileMapping + MapViewOfFile) to shuffle the information from one process到下一个。事件和内存映射文件的(唯一)名称由主进程确定并通过命令行参数传递给看门狗进程。