【问题标题】:SEH on Windows, call stack traceback is goneWindows 上的 SEH,调用堆栈回溯消失了
【发布时间】:2025-12-03 02:15:01
【问题描述】:

我正在阅读关于 Windows 上的 SEH 的 article。 这里是myseh.cpp的源代码

我调试了 myseh.cpp。我分别在第 24 行的 printf("Hello from an exception handler\n"); 和第 36 行的 DWORD handler = (DWORD)_except_handler; 设置了 2 个断点。

然后我运行它,它在第 36 行坏了。我看到堆栈跟踪如下。

由于mov [eax], 1,发生了AccessViolationException 然后它在第 24 行中断。我看到堆栈跟踪如下。

同一线程,但main框架 不见了!而不是_except_handle。而ESP从0018f6c8跳到0018ef340018f6c80018ef34之间差距很大 异常处理后。

我知道_except_handle 必须在用户模式而不是内核模式下运行。 _except_handle 返回后,线程转到 ring0,然后 windows 内核将 CONTEXT EAX 修改为 &scratch & 然后返回 ring3 。因此线程不断运行。

我很好奇windows处理异常的机制: 为什么调用main框架消失了?

为什么 ESP 从0018f6c8 跳转到0018ef34?(我的意思是一个很大的间距),这些 ESP 地址是否属于同一个线程的堆栈?内核是否在 ring3 中对 ESP 玩了一些花样???如果是,为什么选择0018ef34的地址作为handler回调的frame?非常感谢!

【问题讨论】:

  • 是同一个线程的堆栈。内核将CONTEXTEXCEPTION_RECORD 复制到用户线程堆栈。当然,Esp 例外 - 因为这已经严重减少了 Esp。然后从内核调用KiUserExceptionDispatcher 回调,并使用指向此复制的CONTEXTEXCEPTION_RECORD 记录的指针。最后你的_except_handler 打电话了。但是如果您查找ContextRecord->Esp,您会注意到它与main() 中的Esp 完全相同。对于真正的处理程序实现看\VC\crt\src\i386\chandler4.c\VC\crt\src\amd64\chandler.c
  • 是的,我知道 CONTEXT 必须是发生异常时的硬件快照。所以,ContextRecord->Esp==Esp in main()。事实上,我们应该归咎于调试器的默认设置,它丢失了堆栈跟踪。顺便说一句,我知道*ContextRecord 的可修改性,这是内核在返回ring3 之前按照您的意愿恢复硬件上下文的平均值。但为什么是* 类型的第二个参数ExceptionRecord 而不是const *
  • ExceptionRecord 真的不是 const。 ExceptionFlags 在异常处理期间被修改。我们两次通过堆栈走!首先我们寻找__try/__except 块,直到一些不返回EXCEPTION_EXECUTE_HANDLER 然后_except_handlerX 调用RtlUnwindEx,它在ExceptionFlags 中设置EXCEPTION_UNWIND 标志,然后再次遍历__try/__finally 处理程序的堆栈。在winnt.hwdm.h 中寻找IS_DISPATCHING(Flag)IS_UNWINDING(Flag) 宏 - 同时研究chandler.c 注意if (IS_DISPATCHING(ExceptionRecord->ExceptionFlags)) 开关
  • 在 x64 中也使用了 IS_TARGET_UNWIND。简单地说来自myseh.cpp_except_handler 非常原始。支持__try/__except__try/__finally 子调用的真实处理程序用于复杂且需要修改 (RtlUnwindEx) ExceptionFlags 的最小值。也嵌套了ExceptionRecord

标签: windows debugging seh


【解决方案1】:

您使用的是默认调试器设置,不足以查看所有详细信息。选择它们是为了帮助您专注于自己的代码并尽快启动调试会话。

[External Code] 块告诉您堆栈帧的某些部分不属于您编写的代码。他们没有,他们属于操作系统。使用工具 > 选项 > 调试 > 常规并取消选中“仅启用我的代码”选项。

[Frames below may be wrong...] 警告告诉您调试器没有准确的 PDB 来正确遍历堆栈。使用工具 > 选项 > 调试 > 符号并勾选“Microsoft 符号服务器”选项并选择缓存位置。调试器现在将下载您需要通过操作系统 DLL 进行调试的 PDB。可能需要一段时间,它只完成一次。

你可以推断出 ESP 的大变化,CONTEXT 结构非常大,占用了堆栈空间。

在这些更改之后,您现在应该会看到类似的东西:

ConsoleApplication1942.exe!_except_handler(_EXCEPTION_RECORD * ExceptionRecord, void * EstablisherFrame, _CONTEXT * ContextRecord, void * DispatcherContext) Line 22    C++
ntdll.dll!ExecuteHandler2@20()  Unknown
ntdll.dll!ExecuteHandler@20()   Unknown
ntdll.dll!_KiUserExceptionDispatcher@8()    Unknown
ConsoleApplication1942.exe!main() Line 46   C++
ConsoleApplication1942.exe!invoke_main() Line 64    C++
ConsoleApplication1942.exe!__scrt_common_main_seh() Line 255    C++
ConsoleApplication1942.exe!__scrt_common_main() Line 300    C++
ConsoleApplication1942.exe!mainCRTStartup() Line 17 C++
kernel32.dll!@BaseThreadInitThunk@12()  Unknown
ntdll.dll!__RtlUserThreadStart()    Unknown
ntdll.dll!__RtlUserThreadStart@8()  Unknown

在 Win10 版本 1607 和 VS2015 Update 2 上录制。这不是编写 SEH 处理程序的正确方法,请在 this post 中找到更好的示例。

【讨论】:

  • 谢谢。是的,我知道这个例子只是为了讲述它最基本的概念,并不实用。
  • 您能否详细说明为什么这不是编写 SEH 处理程序的正确理由?我问的原因是因为我想实现一小段代码,它将新的处理程序放在 SEH 链表的开头,导致异常,然后从列表中删除处理程序。类似的东西: void foo() { _EXCEPTION_REGISTRATION_RECORD newHandlerStruct; newHandlerStruct.Handler = (PEXCEPTION_ROUTINE) theHandlerFunc; newHandlerStruct.Next = (_EXCEPTION_REGISTRATION_RECORD*)__readfsdword(0x0); __writefsdword(0x0, (DWORD64)&newHandlerStruct); __debugbreak(); }
  • Pietrek 的代码很古老,可以追溯到 64 位操作系统普及和互联网无情地攻击 Windows 程序之前的日子。高度可利用,将数据转换为可执行代码的基本方法。微软不得不想出对策,SafeSEH是最基本的。生成适当的代码是编译器的工作。