TL:DR: 不,有一些 SEH 极端案例在实践中可能使其不安全,并且被记录为不安全。 @Raymond Chen 最近写了一篇博文,您可能应该阅读它而不是这个答案。
他的代码获取页面错误 I/O 错误示例可以通过提示用户插入 CD-ROM 并重试来“修复”,这也是我对唯一实际可恢复的错误的结论,如果没有任何其他错误在 ESP/RSP 下存储和重新加载之间可能存在错误指令。
或者,如果您要求调试器调用被调试程序中的函数,它也会使用目标进程的堆栈。
这个答案列出了一些你认为可能会踩到低于 ESP 的内存的东西,但实际上不会,这可能很有趣。似乎只有 SEH 和调试器在实践中可能会出现问题。
首先,如果你关心效率,你不能在你的调用约定中避免 x87 吗? movd xmm0, eax
是返回float
整数寄存器中的 a 的更有效方法。(并且您通常可以避免将 FP 值首先移动到整数寄存器,使用 SSE2 整数指令来区分 a 的指数/尾数log(x)
,或整数加 1 的nextafter(x)
。)但是如果您需要支持非常旧的硬件,那么您需要程序的 32 位 x87 版本以及高效的 64 位版本。
但是对于堆栈上的少量暂存空间还有其他用例,最好保存一些抵消 ESP/RSP 的指令。
试图在他们下面的评论中收集其他答案和讨论的综合智慧(以及这个答案):
它被Microsoft明确记录为不安全:(对于 64 位代码,我没有找到 32 位代码的等效语句,但我确定有一个)
堆栈使用(对于 x64)
RSP 当前地址之外的所有内存都被认为是易失性的:操作系统或调试器可能会在用户调试会话或中断处理程序期间覆盖此内存。
这就是文档,但是所述的中断原因对用户空间堆栈没有意义,只有内核堆栈才有意义。重要的部分是他们将其记录为不能保证安全,而不是给出的原因。
硬件中断不能使用用户栈;这将使用户空间使内核崩溃mov esp, 0
,或者更糟的是通过在中断处理程序运行时让用户空间进程中的另一个线程修改返回地址来接管内核。这就是为什么内核总是配置一些东西,以便将中断上下文推送到内核堆栈上。
现代调试器在单独的进程中运行,并且不是“侵入式”的。回到 16 位 DOS 时代,没有多任务受保护内存操作系统来为每个任务提供自己的地址空间,调试器将在单步执行时在任意两条指令之间使用与被调试程序相同的堆栈。
@RossRidge 指出,调试器可能希望让您在当前线程的上下文中调用函数,例如 with SetThreadContext
。这将在低于当前值的 ESP/RSP 下运行。这显然会对正在调试的进程产生副作用(运行调试器的用户是故意的),但是破坏 ESP/RSP 以下当前函数的局部变量将是一个不希望的和意想不到的副作用。(所以编译器不能把它们放在那里。)
(在红区低于 ESP/RSP 的调用约定中,调试器可以通过在进行函数调用之前递减 ESP/RSP 来尊重该红区。)
现有的程序在调试时会故意破坏,并将其视为一种功能(以防止对其进行逆向工程)。
相关:x86-64 System V ABI(Linux、OS X、所有其他非 Windows系统)确实为用户空间代码(仅限 64 位)定义了一个红色区域:低于 RSP 的 128 个字节,保证不会异步破坏。Unix 信号处理程序可以在任何两个用户空间指令之间异步运行,但内核通过在旧用户空间 RSP 下方留下 128 字节间隙来尊重红色区域,以防它正在使用中。在没有安装信号处理程序的情况下,即使在 32 位模式下(ABI 不保证红色区域),您也有一个有效的无限红色区域。编译器生成的代码或库代码当然不能假设整个程序(或程序调用的库中)没有安装信号处理程序。
所以问题就变成了:Windows 上是否有任何东西可以在两条任意指令之间使用用户空间堆栈异步运行代码?(即任何等价于 Unix 信号处理程序。)
据我们所知,SEH(结构化异常处理)是您在当前32 位和 64 位 Windows上为用户空间代码建议的唯一真正障碍。 (但未来的 Windows 可能会包含一个新功能。)我猜如果你碰巧要求你的调试器在目标进程/线程中调用一个函数,如上所述。
在这种特定情况下,不接触堆栈以外的任何其他内存,或者做任何其他可能出错的事情,即使是 SEH 也可能是安全的。
SEH(结构化异常处理)让用户空间软件具有硬件异常,例如除以零,交付方式与 C++ 异常有些相似。这些并不是真正的异步:它们是针对由您运行的指令触发的异常,而不是针对发生在某些随机指令之后的事件。
但与正常异常不同,SEH 处理程序可以做的一件事是从异常发生的地方恢复。(@RossRidge 评论:SEH 处理程序最初是在展开堆栈的上下文中调用的,并且可以选择忽略异常并在发生异常的点继续执行。)
所以即使catch()
当前函数中没有子句,这也是一个问题。
通常只能同步触发硬件异常。例如,通过div
指令或内存访问可能出错STATUS_ACCESS_VIOLATION
(Windows 相当于 Linux SIGSEGV 分段错误)。您可以控制使用的指令,因此您可以避免可能出错的指令。
如果您将代码限制为仅在存储和重新加载之间访问堆栈内存,并且您尊重堆栈增长保护页面,那么您的程序将不会因访问而出错[esp-4]
。(除非您达到最大堆栈大小(堆栈溢出),在这种情况下push eax
也会出错,并且您无法真正从这种情况中恢复,因为没有可供 SEH 使用的堆栈空间。)
所以我们可以排除STATUS_ACCESS_VIOLATION
这个问题,因为如果我们在访问堆栈内存时得到它,我们无论如何都会被冲洗掉。
一个 SEH 处理程序STATUS_IN_PAGE_ERROR
可以在任何加载指令之前运行。 Windows 可以分页它想要的任何页面,并在再次需要时透明地分页(虚拟内存分页)。但是,如果出现 I/O 错误,您的 Windows 会尝试让您的进程通过提供一个STATUS_IN_PAGE_ERROR
同样,如果这发生在当前堆栈上,我们就会被淹没。
但是 code-fetch 可能会导致STATUS_IN_PAGE_ERROR
,并且您可以合理地从中恢复。但不是通过在发生异常的地方恢复执行(除非我们可以以某种方式将该页面重新映射到高度容错系统中的另一个副本??),所以我们在这里可能仍然可以。
代码中的 I/O 错误分页想要读取我们存储在 ESP 下的内容,排除了任何读取它的机会。如果你不打算这样做,那你很好。 一个不知道这段特定代码的通用 SEH 处理程序无论如何都不会尝试这样做。我认为通常STATUS_IN_PAGE_ERROR
最多会尝试打印错误消息或记录某些内容,而不是尝试进行正在发生的任何计算。
访问存储之间的其他内存并重新加载到 ESP 之下的内存可能会触发STATUS_IN_PAGE_ERROR
该内存的 a 。在库代码中,您可能无法假设您传递的其他一些指针不会很奇怪,并且调用者期望STATUS_ACCESS_VIOLATION
为它处理或 PAGE_ERROR。
当前的编译器没有利用 Windows 上 ESP/RSP 下方的空间,即使它们确实利用了 x86-64 System V 中的红色区域(在需要溢出/重新加载某些东西的叶函数中,就像你的那样)重新为 int -> x87 做。)那是因为 MS 说它不安全,而且他们不知道是否存在可以在 SEH 之后尝试恢复的 SEH 处理程序。
您认为在当前 Windows 中可能存在问题的事情,以及为什么不是:
ESP 下方的保护页内容:只要您不低于当前 ESP 太远,您就会接触到保护页并触发更多堆栈空间的分配,而不是出错。这很好,只要内核不检查用户空间 ESP 并发现您在没有先“保留”它的情况下接触堆栈空间。
内核回收低于 ESP/RSP 的页面:显然 Windows 目前不这样做。因此,一次使用大量堆栈空间将使这些页面在您的进程生命周期的剩余时间内保持分配,除非您手动VirtualAlloc(MEM_RESET)
将它们分配。(不过,内核将被允许这样做,因为文档说 RSP 以下的内存是易失性的。如果内核愿意,内核可以有效地将其异步归零,写时复制将其映射到零页面而不是将其写入内存压力下的页面文件。)
APC(异步过程调用):它们只能在进程处于“警报状态”时交付,这意味着只有在call
一个函数内部时,如SleepEx(0,1)
. call
一个函数已经在 E/RSP 下面使用了未知数量的空间,所以你已经必须假设每个都call
破坏了堆栈指针下面的所有内容。因此,这些“异步”回调对于正常执行而言并不是真正异步的,就像 Unix 信号处理程序那样。(有趣的事实:POSIX async io 确实使用信号处理程序来运行回调)。
ctrl-C 和其他事件的控制台应用程序回调 ( SetConsoleCtrlHandler
)。这看起来与注册 Unix 信号处理程序完全一样,但在 Windows 中,处理程序在具有自己堆栈的单独线程中运行。(见 RbMm 的评论)
SetThreadContext
: 当这个线程被挂起时,另一个线程可以异步地改变我们的 EIP/RIP,但是整个程序必须专门为此编写才有意义。除非它是使用它的调试器。当一些其他线程在你的 EIP 中乱作一团时,通常不需要正确性,除非情况得到很好的控制。
显然,没有其他方法可以让另一个进程(或该线程注册的东西)触发与 Windows 上用户空间代码执行异步执行的任何操作。
如果没有可以尝试恢复的 SEH 处理程序,Windows 或多或少在 ESP 下方有一个 4096 字节的红色区域(或者如果您逐渐触摸它可能会更多?),但 RbMm 表示在实践中没有人利用它。这并不奇怪,因为 MS 说不要,而且你不能总是知道你的调用者是否对 SEH 做了什么。
显然,任何会同步破坏它的东西(如 a call
)也必须避免,这与在 x86-64 System V 调用约定中使用 red-zone 时相同。(有关更多信息,请参阅https://stackoverflow.com/tags/red-zone/info。)