为什么64位Windows不能在异常期间展开堆栈,如果堆栈跨越内核边界 - 当32位Windows可以?
整个问题的背景来自:
The case of the disappearing OnLoad exception – user-mode callback exceptions in x64
在32位Windows中,如果我在用户模式代码中抛出异常,那是从内核模式代码中调用的,那是从我的调用的>用户模式代码,例如:
User mode Kernel Mode
------------------ -------------------
CreateWindow(...); ------> NtCreateWindow(...)
|
WindowProc <---------------------+
Windows中的结构化异常处理(SEH)可以展开堆栈,通过内核模式展开回到我的用户代码,在那里我可以处理异常并看到有效的堆栈跟踪。
64位版本的Windows无法执行此操作:
由于复杂的原因,我们无法在64位操作系统上传播异常(amd64和IA64)。自从Server 2003的第一个64位版本发布以来,情况一直如此。在x86上,情况并非如此 - 异常通过内核边界传播并最终将帧移回框架
由于在这种情况下无法回溯可靠的堆栈跟踪,因此必须做出决定:让您看到非荒谬的异常,或者完全隐藏它:
当时的内核架构师决定采取保守的AppCompat友好方法 - 隐藏异常,并希望最好。
本文接着讨论了所有64位Windows操作系统的运行方式:
但是从Windows 7(和Windows Server 2008)开始,架构师改变了主意 - 有点像。对于 仅 64位应用程序(不是32位应用程序),它们(默认情况下)停止抑制这些用户内核用户异常。因此,默认情况下,在:
所有64位应用程序都会看到这些异常,他们从来没有看到它们。
在Windows 7中,当本机x64 应用程序以这种方式崩溃时,会通知Program Compatibility Assistant。如果应用程序没有Windows 7 Manifest,我们会显示一个对话框,告诉您PCA已应用Application Compatibility shim。这是什么意思?这意味着,下次运行应用程序时,Windows将模拟Server 2003行为并使异常消失。请记住,Server 2008 R2上不存在PCA,因此该建议不适用。
问题是为什么 64位Windows无法通过内核转换展开堆栈,而32位版本的Windows可以吗?
唯一的提示是:
由于复杂的原因,我们无法在64位操作系统上传播异常(amd64和IA64)。
提示它很复杂。
我可能不理解这个解释,因为我不是一个操作系统开发人员 - 但我想知道为什么会这样做。
Microsoft已发布a hotfix enables 32-bit applications也不再禁止例外:
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
KB973460中的XP和Server 2003上也记录了这种行为:
我在调查使用xperf捕获64位Windows上的堆栈跟踪时发现了另一个提示:
禁用分页执行
要使跟踪在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)中,不允许您遍历内核堆栈,因为磁盘上可能存在页面。
答案 0 :(得分:16)
我是时间以前写过这个Hotfix的开发人员以及博客文章。主要原因是,出于性能原因,当您转换到内核空间时,不会始终捕获完整的寄存器文件。
如果进行正常的系统调用,x64 Application Binary Interface(ABI)只需要保留the non-volatile registers(类似于进行正常的函数调用)。但是,正确展开异常需要您拥有所有寄存器,因此无法实现。基本上,这是在关键场景中的perf(即可能每秒发生数千次的场景)与100%正确处理病态场景(崩溃)之间的选择。
答案 1 :(得分:8)
一个非常好的问题。
我可以给出一个暗示为什么跨内核用户边界“传播”异常有些问题。
引用你的问题:
为什么64位Windows在异常期间不能展开堆栈,如果堆栈越过内核边界 - 当32位Windows可以时?
原因很简单:没有“堆栈跨越内核边界”这样的东西。调用内核模式函数绝不能与标准函数调用相比。它实际上与调用堆栈无关。您可能知道,内核模式内存在用户模式下无法访问。
通过触发软件中断(或类似机制)来实现调用内核模式函数(又名 syscall )。用户模式代码将一些值放入寄存器(标识所需的内核模式服务)并调用CPU指令(例如sysenter
),该指令将CPU传输到内核模式并将控制传递给OS。
然后有一个内核模式代码来处理请求的系统调用。它在一个单独的内核模式堆栈中运行(与用户模式堆栈无关)。处理完请求后 - 控件返回到用户模式代码。根据特定的系统调用,用户模式返回地址可能是调用内核模式事务的地址,也可能是不同的地址。
有时你会调用一个“中间”应该调用用户模式调用的内核模式函数。它可能看起来像一个由用户内核用户代码组成的调用堆栈,但它只是一个仿真。在这种情况下,内核模式代码将控件传输到用户模式代码,该代码包装您的用户模式功能。这个包装器代码调用你的函数,并在它返回时立即触发内核模式事务。
现在,如果用户模式代码“从kernelmode调用”引发异常 - 这就应该发生:
因此跨越内核 - 用户边界的异常是仿真。本机没有这样的东西。