For*_*VeR 5 c++ debugging winapi
在编写基于 x86 WinAPI 的调试器时,我遇到了一种罕见的情况,即调试对象(通常运行良好)突然终止EXCEPTION_ACCESS_VIOLATION
我使用本机调试器附加到调试对象(通常运行良好)后突然终止。我可以在任何应用程序上稳定地重现这一点(在 .NET Hello World 风格的应用程序和notepad.exe
多台 Windows 10 计算机上尝试过)。
本质上我写了一个简单的WaitForDebugEvent
循环:
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\nCREATE_PROCESS_DEBUG_EVENT
(报告进程及其初始线程的创建)LOAD_DLL_DEBUG_EVENT
(我一直无法获得这个 DLL 的名称,但这在 MSDN 中有记录)CREATE_THREAD_DEBUG_EVENT
(我怀疑这是调试器注入的线程)LOAD_DLL_DEBUG_EVENT
[\xe2\x80\xa6] \xe2\x80\x94 之后,许多 DLL 被加载到目标进程中,一切看起来都很好,进程按预期工作但有时(大约占所有运行的 1.5%),事件顺序会发生变化:
\n\nCREATE_PROCESS_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
CREATE_THREAD_DEBUG_EVENT
EXCEPTION_DEBUG_EVENT: EXCEPTION_ACCESS_VIOLATION
(我一直无法收集详细信息:它报告了 DEP 违规,并且地址为空)之后,我无法继续调试,因为我的调试对象处于异常状态并且很快就会终止。如果没有附加调试器,我永远无法捕获notepad.exe
崩溃(我怀疑它有那么糟糕并且会无缘无故地崩溃),所以我怀疑我的调试器导致了这些异常。
一个奇怪的细节是,我可以Sleep(1)
在之后立即打电话来“解决”这种情况WaitForDebugEvent
。那么,这可能是某种竞争条件,但是什么之间的竞争条件呢?在调试器线程和被调试者中的其他线程之间?这是一件事吗?那么我们应该如何调试其他应用程序呢?如果它是一个东西,实际的调试器如何工作?
我无法使用为 x64 CPU 编译的相同代码(并调试 x64 进程)重现该问题。
\n\n实际上什么可能导致这种错误行为?我仔细阅读了有关我调用的 API 函数的文档,并在线检查了一些其他调试器示例,但仍然无法找到我的调试器出了什么问题:看起来我遵循了所有正确的约定。
\n\n我尝试使用 WinDBG 调试我的调试对象,同时它仍然在我的调试器中暂停,但没有成功。首先,很难用另一个调试器附加到调试对象(WinDBG 只允许使用非侵入模式,这看起来功能较少?),并且进程线程的调用堆栈是通常有意义。
\n\n检查这个存储库,用MSVC编译然后执行cmd
:
Debug\\NetRuntimeWaiter.exe > log.txt\n
Run Code Online (Sandbox Code Playgroud)\n\n将输出重定向到日志文件而不是在终端中显示它非常重要:否则,日志编写器的计时会发生变化,并且问题将不会重现(由于我之前提到的可能的竞争条件?)。
\n\n通常程序将在大约 10 秒内启动和终止 1000 个记事本,并且 1000 次调用中的 10-15 次将保留错误条件(即EXCEPTION_ACCESS_VIOLATION
)。
(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 )创建被调试进程,我们可以执行以下操作:
DbgUiSetThreadDebugObject(hDdg)
调用之前
调用CreateProcessW
DEBUG_ONLY_THIS_PROCESS
DEBUG_PROCESS
DbgUiGetThreadDebugObject()
从线程获取调试对象并将其传递给低级进程创建 apiDbgUiSetThreadDebugObject(0)
实际上,无论谁使用调试对象创建进程。无论谁处理发布到此调试对象的事件。
您可以从ntdbg.h获取所有未记录的 api 定义,然后与ntdll.lib或ntdllp.lib链接