为什么64位Windows无法解除用户内核用户异常?

Ian*_*oyd 37 windows 64-bit structured-exception windows64 windows-appcompat-platform

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

整个问题的背景来自:

消失的OnLoad异常 - x64中的用户模式回调异常的情况

背景

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

User mode                     Kernel Mode
------------------            -------------------
CreateWindow(...);   ------>  NtCreateWindow(...)
                                   |
WindowProc   <---------------------+                                   
Run Code Online (Sandbox Code Playgroud)

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

但不是在64位Windows中

64位版本的Windows无法执行此操作:

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

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

当时的内核架构师决定采用保守的AppCompat友好方法 - 隐藏异常,并希望最好.

本文接着讨论了所有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位
  • Windows Server 2008

所有64位应用程序都会看到这些异常,他们从来没有看到它们.

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

所以问题

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

唯一的提示是:

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

暗示是复杂的.

我可能不理解这个解释,因为我不是一个操作系统开发人员 - 但我想知道为什么会这样做.


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

微软发布了一个修复程序,使32位应用程序也不再有被抑制的异常:

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

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

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

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

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

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
DisableUserModeCallbackFilter: DWORD = 1
Run Code Online (Sandbox Code Playgroud)

或每次申请:

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\Notepad.exe
DisableUserModeCallbackFilter: DWORD = 1
Run Code Online (Sandbox Code Playgroud)

在KB973460中的XP和Server 2003上也记录了这种行为:


一个提示

我在调查使用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
Run Code Online (Sandbox Code Playgroud)

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

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

Ana*_*tts 15

我是开发人员之前写过这个Hotfix的loooooooong以及博客文章.主要原因是,出于性能原因,当您转换到内核空间时,并不总是捕获完整的寄存器文件.

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

奖金阅读

  • 这个答案对我来说毫无意义.x86和x64 ABI都有调用者保存/易失性寄存器,所以没有区别.展开不需要知道所有寄存器的值,只需要知道那些没有死的寄存器的值,如果你不保存它们,即使在正常执行中也会有不好的时间.这个答案中没有任何内容似乎特定于内核用户转换或64位代码,所以我仍然对这种组合的哪些特殊功能使异常处理更加困难感到困惑. (3认同)
  • 我很抱歉,但我认为你是错误的记忆,除非你或某人能够解释它在理论上甚至可能是真的.确实,异常处理可能需要保存更多寄存器,因为控制流图中存在影响活跃度分析和可能的寄存器分配的额外依赖性.微软可能已经决定他们不希望64位内核的开销.这与您所写的内容不一致,因为它与架构或ABI无关,但是您可能会因为与ABI相关而错误地记录. (2认同)

val*_*ldo 8

一个非常好的问题.

我可以给出一个暗示为什么跨内核用户边界"传播"异常有些问题.

引用你的问题:

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

原因很简单:没有"堆栈跨越内核边界"这样的东西.调用内核模式函数绝不能与标准函数调用相比.它实际上与调用堆栈无关.您可能知道,内核模式内存在用户模式下无法访问.

调用内核模式函数(也称为syscall)是通过触发软件中断(或类似机制)来实现的.用户模式代码将一些值放入寄存器(标识所需的内核模式服务)并调用CPU指令(例如sysenter),该指令将CPU传输到内核模式并将控制传递给OS.

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

有时你会调用一个"中间"应该调用用户模式调用的内核模式函数.它可能看起来像一个由用户内核用户代码组成的调用堆栈,但它只是一个仿真.在这种情况下,内核模式代码将控件传输到用户模式代码,该代码包装您的用户模式功能.此包装器代码调用您的函数,并在其返回时立即触发内核模式事务.

现在,如果用户模式代码"从内核模式调用"引发了异常 - 这就应该发生:

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

因此跨越内核用户边界的异常是一种仿真.本机没有这样的东西.

  • 令人困惑的是,微软在1993年创建Windows NT时解决了这一切.虽然您的答案确实提供了用户内核模式调用转换的很好的概述. (3认同)
  • 我的意思是,让我们不要发疯,sysenter/sysexit和调用/ ret到固定地址的数量级不同.这真的只是代码一直向下.你是对的,如果你想要展开整个堆栈,你必须特殊情况下转换点 (3认同)