调试以 CREATE_SUSPENDED 启动的任何进程时,罕见 EXCEPTION_ACCESS_VIOLATION

For*_*VeR 5 c++ debugging winapi

在编写基于 x86 WinAPI 的调试器时,我遇到了一种罕见的情况,即调试对象(通常运行良好)突然终止EXCEPTION_ACCESS_VIOLATION我使用本机调试器附加到调试对象(通常运行良好)后突然终止。我可以在任何应用程序上稳定地重现这一点(在 .NET Hello World 风格的应用程序和notepad.exe多台 Windows 10 计算机上尝试过)。

\n\n

本质上我写了一个简单的WaitForDebugEvent循环:

\n\n
CreateProcessW(L"C:\\\\Windows\\\\SYSWOW64\\\\notepad.exe", [\xe2\x80\xa6], CREATE_SUSPENDED, [\xe2\x80\xa6]);\nDebugActiveProcess(processId);\n\nDEBUG_EVENT debugEvent = {};\nwhile (WaitForDebugEvent(&debugEvent, INFINITE)) {\n  switch (debugEvent.dwDebugEventCode) {\n    // log all the events\n  }\n  ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED);\n}\n\nDebugActiveProcessStop(processId);\n
Run Code Online (Sandbox Code Playgroud)\n\n

这是完整列表:我不会将其全部粘贴到这里,因为那里有一些额外的非必要的样板;MCVE 有 136 行长)

\n\n

为了举例,我将只记录所有调试器事件并检测调试对象是否准备好“正常进行”,或者是否会因异常而终止。

\n\n

大多数时候,我的调试会话如下所示:

\n\n
    \n
  • CREATE_PROCESS_DEBUG_EVENT(报告进程及其初始线程的创建)
  • \n
  • LOAD_DLL_DEBUG_EVENT(我一直无法获得这个 DLL 的名称,但这在 MSDN 中有记录)
  • \n
  • CREATE_THREAD_DEBUG_EVENT(我怀疑这是调试器注入的线程)
  • \n
  • LOAD_DLL_DEBUG_EVENT[\xe2\x80\xa6] \xe2\x80\x94 之后,许多 DLL 被加载到目标进程中,一切看起来都很好,进程按预期工作
  • \n
\n\n

但有时(大约占所有运行的 1.5%),事件顺序会发生变化:

\n\n
    \n
  • CREATE_PROCESS_DEBUG_EVENT
  • \n
  • LOAD_DLL_DEBUG_EVENT
  • \n
  • CREATE_THREAD_DEBUG_EVENT
  • \n
  • EXCEPTION_DEBUG_EVENT: EXCEPTION_ACCESS_VIOLATION(我一直无法收集详细信息:它报告了 DEP 违规,并且地址为空)
  • \n
\n\n

之后,我无法继续调试,因为我的调试对象处于异常状态并且很快就会终止。如果没有附加调试器,我永远无法捕获notepad.exe崩溃(我怀疑它有那么糟糕并且会无缘无故地崩溃),所以我怀疑我的调试器导致了这些异常。

\n\n

一个奇怪的细节是,我可以Sleep(1)在之后立即打电话来“解决”这种情况WaitForDebugEvent。那么,这可能是某种竞争条件,但是什么之间的竞争条件呢?在调试器线程和被调试者中的其他线程之间?这是一件事吗?那么我们应该如何调试其他应用程序呢?如果它是一个东西,实际的调试器如何工作?

\n\n

我无法使用为 x64 CPU 编译的相同代码(并调试 x64 进程)重现该问题。

\n\n

实际上什么可能导致这种错误行为?我仔细阅读了有关我调用的 API 函数的文档,并在线检查了一些其他调试器示例,但仍然无法找到我的调试器出了什么问题:看起来我遵循了所有正确的约定。

\n\n

我尝试使用 WinDBG 调试我的调试对象,同时它仍然在我的调试器中暂停,但没有成功。首先,很难用另一个调试器附加到调试对象(WinDBG 只允许使用非侵入模式,这看起来功能较少?),并且进程线程的调用堆栈是通常有意义。

\n\n

重现步骤

\n\n

检查这个存储库,用MSVC编译然后执行cmd

\n\n
Debug\\NetRuntimeWaiter.exe > log.txt\n
Run Code Online (Sandbox Code Playgroud)\n\n

将输出重定向到日志文件而不是在终端中显示它非常重要:否则,日志编写器的计时会发生变化,并且问题将不会重现(由于我之前提到的可能的竞争条件?)。

\n\n

通常程序将在大约 10 秒内启动和终止 1000 个记事本,并且 1000 次调用中的 10-15 次将保留错误条件(即EXCEPTION_ACCESS_VIOLATION)。

\n

RbM*_*bMm 4

DebugActiveProcess以及DbgUiDebugActiveProcess由 内部调用的未记录的DebugActiveProcess)有严重的设计问题:调用它后,NtDebugActiveProcess通过调用在目标进程中创建远程线程DbgUiIssueRemoteBreakin- 结果在目标进程中创建了新线程 - DbgUiRemoteBreakin- 该线程调用DbgBreakPoint,然后RtlExitUserThread

所有这些都没有记录和解释,只有以下注释DebugActiveProcess

所有这些完成后,系统恢复进程中的所有线程。当进程中的第一个线程恢复时,它会执行断点指令,从而将EXCEPTION_DEBUG_EVENT 调试事件发送到调试器。

当然这是错误的。为什么是DbgUiRemoteBreakin第一个(??)线程?以及哪个线程首先恢复未定义。为什么不完全写 - 我们在进程中创建附加(但不是第一个)线程?并且该线程执行断点。

但是,当进程已经运行时 - 创建这个附加线程不会产生问题。但如果我们在挂起状态下创建进程,然后调用DebugActiveProcess-DbgUiRemoteBreakin真正成为进程中的第一个执行线程,并且进程初始化是在该线程上完成的,而不是创建的第一个线程。在 xp 上,这总是会导致连接到 csrss 阶段的进程初始化失败。(csrss wait 仅在进程中第一个创建的线程上连接到它)。在后来的系统上,这是固定的,进程可以照常执行。但可以也不能,因为初始化它的线程已退出。它可能会导致微妙的问题。

这里的解决方案-不是使用DebugActiveProcess而是NtDebugActiveProcess放在它的地方。我们可以创建或通过调试对象DbgUiConnectToDbg(),然后通过(线程TEBDbgUiGetThreadDebugObject()中的系统存储调试对象)或直接通过调用获取它NtCreateDebugObject

另外,如果我们从另一个进程( B )创建被调试进程,我们可以执行以下操作:

  • 将调试对象从调试器进程复制到此B进程
  • 在使用orDbgUiSetThreadDebugObject(hDdg)调用之前 调用CreateProcessWDEBUG_ONLY_THIS_PROCESSDEBUG_PROCESS
  • 系统将用于DbgUiGetThreadDebugObject()从线程获取调试对象并将其传递给低级进程创建 api
  • 通过从线程中删除调试对象 DbgUiSetThreadDebugObject(0)

实际上,无论谁使用调试对象创建进程。无论谁处理发布到此调试对象的事件。

您可以从ntdbg.h获取所有未记录的 api 定义,然后与ntdll.libntdllp.lib链接