37

如果堆栈跨越内核边界,为什么 64 位 Windows 不能在异常期间展开堆栈 - 而 32 位 Windows 可以?

整个问题的背景来自:

OnLoad 异常消失案例——x64 中的用户态回调异常

背景

在 32 位 Windows 中,如果我在我的用户模式代码中抛出异常,该异常是从内核模式代码回调的,它是从我的用户模式代码调用的,例如:

User mode                     Kernel Mode
------------------            -------------------
CreateWindow(...);   ------>  NtCreateWindow(...)
                                   |
WindowProc   <---------------------+                                   

Windows 中的结构化异常处理 (SEH) 可以展开堆栈,通过内核模式展开,回到我的用户代码,在那里我可以处理异常并且我看到有效的堆栈跟踪。

但不是在 64 位 Windows 中

64 位版本的 Windows 不能这样做:

由于复杂的原因,我们无法将异常传播回 64 位操作系统(amd64 和 IA64)。自从 Server 2003 的第一个 64 位版本以来,情况就一直如此。在 x86 上,情况并非如此——异常通过内核边界传播,最终将帧返回

而且由于在这种情况下无法返回可靠的堆栈跟踪,因此必须做出决定:让您看到无意义的异常,或者完全隐藏它:

当时的内核架构师决定采取保守的 AppCompat-friendly 方法——隐藏异常,并寄希望于最好的结果。

本文继续讨论所有 64 位 Windows 操作系统的行为方式:

  • Windows XP 64 位
  • Windows Server 2003 64 位
  • Windows Vista 64 位
  • Windows Server 2008 64 位

但从 Windows 7(和 Windows Server 2008)开始,架构师改变了他们的想法——在某种程度上。对于64 位应用程序(不是 32 位应用程序),它们将(默认情况下)停止抑制这些用户-内核-用户异常。因此,默认情况下,打开:

  • Windows 7 64 位
  • 视窗服务器 2008

所有 64 位应用程序都会看到这些异常,它们以前从未见过它们。

在 Windows 7 中,当原生 x64应用程序以这种方式崩溃时,程序兼容性助手会收到通知。如果应用程序没有Windows 7 清单,我们会显示一个对话框,告诉您 PCA 已应用应用程序兼容性 shim。这是什么意思?这意味着,下次您运行应用程序时,Windows 将模拟 Server 2003 的行为并使异常消失。请记住,Server 2008 R2 上不存在 PCA,因此此建议不适用。

所以这个问题

问题是为什么64 位 Windows 无法通过内核转换来展开堆栈,而 32 位版本的 Windows 可以?

唯一的提示是:

由于复杂的原因,我们无法将异常传播回 64 位操作系统(amd64 和 IA64)。

提示是复杂的

我可能不明白这个解释,因为我不是操作系统开发人员 - 但我想知道为什么。


更新:停止抑制 32 位应用程序的修补程序

Microsoft 发布了一个修补程序,使 32 位应用程序也不再抑制异常:

KB976038:在 64 位版本的 Windows 中运行的应用程序引发的异常被忽略

  • 在回调例程中引发的异常在用户模式下运行。

在这种情况下,此异常不会导致应用程序崩溃。相反,应用程序进入不一致的状态。然后,应用程序抛出一个不同的异常并崩溃。

用户模式回调函数通常是由内核模式组件调用的应用程序定义的函数。用户模式回调函数的示例是 Windows 过程和挂钩过程。Windows 调用这些函数来处理 Windows 消息或处理 Windows 挂钩事件。

然后,此修补程序可让您阻止 Windows 全局吃掉异常:

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
DisableUserModeCallbackFilter: DWORD = 1

或每个应用程序:

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\Notepad.exe
DisableUserModeCallbackFilter: DWORD = 1

该行为也记录在 XP 和 Server 2003 的 KB973460 中:


一个提示

在研究使用 xperf 在 64 位 Windows 上捕获堆栈跟踪时,我发现了另一个提示:

Xperf 中的堆栈行走

禁用寻呼执行器

为了在 64 位 Windows 上进行跟踪,您需要设置DisablePagingExecutive注册表项。这告诉操作系统不要将内核模式驱动程序和系统代码分页到磁盘,这是使用 xperf 获取 64 位调用堆栈的先决条件,因为 64 位堆栈遍历依赖于可执行映像中的元数据,并且在某些情况下xperf堆栈遍历代码不允许触摸分页页面。从提升的命令提示符运行以下命令将为您设置此注册表项。

 REG ADD "HKLM\System\CurrentControlSet\Control\Session Manager\Memory Management" -v 
 DisablePagingExecutive -d 0x1 -t REG_DWORD -f

设置此注册表项后,您需要重新启动系统才能记录调用堆栈。设置此标志意味着 Windows 内核将更多页面锁定到 RAM 中,因此这可能会消耗大约 10 MB 的额外物理内存。

这给人的印象是,在 64 位 Windows 中(并且仅在 64 位 Windows 中),您不允许遍历内核堆栈,因为磁盘上可能有页面输出。

4

2 回答 2

16

我是很久以前编写此修补程序以及博客文章的开发人员。主要原因是由于性能原因,当您转换到内核空间时,并不总是捕获完整的寄存器文件。

如果您进行正常的系统调用,x64应用程序二进制接口(ABI) 只需要您保留非易失性寄存器(类似于进行正常的函数调用)。但是,正确展开异常需要您拥有所有寄存器,因此这是不可能的。基本上,这是在关键场景中的性能(即每秒可能发生数千次的场景)与 100% 正确处理病态场景(崩溃)之间的选择。

奖金阅读

于 2014-03-14T00:13:21.257 回答
9

一个非常好的问题。

我可以提示为什么跨内核用户边界“传播”异常有些问题。

引用您的问题:

如果堆栈跨越内核边界,为什么 64 位 Windows 不能在异常期间展开堆栈- 而 32 位 Windows 可以?

原因很简单:不存在“堆栈跨越内核边界”这样的事情。调用内核模式函数决不能与标准函数调用相提并论。它实际上与调用堆栈无关。您可能知道,内核模式内存根本无法从用户模式访问。

调用内核模式函数(又名syscall)是通过触发软件中断(或类似机制)来实现的。用户模式代码将一些值放入寄存器(标识所需的内核模式服务)并调用 CPU 指令(例如sysenter),该指令将 CPU 转换为内核模式并将控制权传递给操作系统。

然后是处理请求的系统调用的内核模式代码。它在单独的内核模式堆栈中运行(与用户模式堆栈无关)。处理请求后 - 控件返回到用户模式代码。根据特定的系统调用,用户模式返回地址可能是调用内核模式事务的地址,也可能是不同的地址。

有时你调用一个内核模式函数,“在中间”应该调用一个用户模式调用。它可能看起来像一个由用户-内核-用户代码组成的调用堆栈,但它只是一个仿真。在这种情况下,内核模式代码将控制权转移到包装用户模式函数的用户模式代码。此包装器代码调用您的函数,并在其返回后立即触发内核模式事务。

现在,如果“从内核模式调用”的用户模式代码引发异常 - 这应该会发生:

  1. 包装用户模式代码处理 SEH 异常(即停止其传播,但尚未执行堆栈展开)。
  2. 将控制权传递给内核模式 (OS),就像在正常的程序流案例中一样。
  3. Kenrel 模式代码会做出适当的响应。它完成了请求的服务。根据是否存在用户模式异常 - 处理可能会有所不同。
  4. 返回用户模式后 - 内核模式代码可以指定是否存在嵌套异常。如果发生异常,堆栈不会恢复到其原始状态(因为还没有展开)。
  5. 用户模式代码检查是否存在此类异常。如果是 - 调用堆栈被伪造以包含嵌套的用户模式调用,并且异常传播。

因此,跨越内核用户边界的异常是emulation。国内是没有这种东西的。

于 2012-08-20T11:54:12.027 回答