在ESP下面写下是否有效?

J..*_*... 11 windows x86 assembly abi

对于32位Windows应用程序,使用ESP下面的堆栈内存进行临时交换空间而不显式减少ESP是有效的吗?

考虑一个返回浮点值的函数ST(0).如果我们的价值目前在EAX,我们会,例如,

PUSH   EAX
FLD    [ESP]
ADD    ESP,4  // or POP EAX, etc
// return...
Run Code Online (Sandbox Code Playgroud)

或者不修改ESP寄存器,我们可以:

MOV    [ESP-4], EAX
FLD    [ESP-4]
// return...
Run Code Online (Sandbox Code Playgroud)

在这两种情况下都会发生同样的事情,除了在第一种情况下我们注意在使用内存之前递减堆栈指针,然后在之后递增它.在后一种情况下,我们没有.

尽管有任何实际需要在堆栈上持久保存这个值(重入问题,在PUSH返回值和读取值之间的函数调用等),有没有任何根本原因,为什么写入ESP下面的堆栈这样会无效?

Pet*_*des 10

TL:DR:不,有一些SEH角落案例可能使其在实践中不安全,并被记录为不安全. @Raymond Chen最近写了一篇博文,你应该阅读而不是这个答案.

他的代码获取页面错误I/O错误的例子可以通过提示用户插入CD-ROM并重试来"修复",这也是我唯一可以恢复的故障的结论,如果没有其他的在ESP/RSP下面存储和重新加载之间可能出错的指令.

或者,如果您要求调试器调用正在调试的程序中的函数,它也将使用目标进程的堆栈.

这个答案列出了一些你认为可能会在ESP下面记忆的东西,但实际上并没有,这可能很有趣.似乎只有SEH和调试器在实践中可能是一个问题.



首先,如果你关心效率,你不能在你的召唤惯例中避免使用x87吗? movd xmm0, eax是一种更有效的方法来返回float整数寄存器中的一个.(并且您通常可以避免首先将FP值移动到整数寄存器,使用SSE2整数指令来选择指数/尾数为a log(x)或整数加1.nextafter(x))但是如果您需要支持非常旧的硬件,那么您需要一个32位x87版本的程序以及一个高效的64位版本.

但是对于堆栈上的少量暂存空间还有其他用例,可以保存几个偏移ESP/RSP的指令.


试图在他们的评论中收集其他答案和讨论的综合智慧(以及这个答案):

它是明确记录为没有通过微软的安全:(64位码,我没找到32位代码的等价语句,但我敢肯定有一个)

堆栈使用(适用于x64)

超出RSP当前地址的所有内存都被视为易失性:操作系统或调试器可能会在用户调试会话或中断处理程序期间覆盖此内存.

这就是文档,但所说的中断原因对用户空间堆栈没有意义,只对内核堆栈有意义.重要的是,他们将其记录为不保证安全,而不是给出的理由.

硬件中断不能使用用户堆栈; 这会让用户空间崩溃内核mov esp, 0,或者更糟糕的是通过让用户空间进程中的另一个线程在中断处理程序运行时修改返回地址来接管内核.这就是为什么内核总是配置东西,所以中断上下文被推送到内核堆栈.

现代调试器在一个单独的进程中运行,并不是"侵入式".回到16位DOS时代,没有多任务保护内存操作系统为每个任务提供自己的地址空间,调试器将使用与正在调试的程序相同的堆栈,在单步执行的任何两个指令之间.

@RossRidge指出调试器可能希望让你在当前线程的上下文中调用一个函数,例如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].(除非你达到最大堆栈大小(Stack Overflow),在这种情况下push eax也会出错,并且你无法从这种情况中恢复,因为没有可供SEH使用的堆栈空间.)

所以我们可以排除STATUS_ACCESS_VIOLATION这个问题,因为如果我们在访问堆栈内存时得到了这个问题,我们就会被冲洗掉.

SEH处理程序STATUS_IN_PAGE_ERROR可以在任何加载指令之前运行. Windows可以将其想要的任何页面分页,并在需要时再透明地将其重新分页(虚拟内存分页).但是如果出现I/O错误,Windows会尝试让您的进程通过提供a来处理失败STATUS_IN_PAGE_ERROR

同样,如果当前的堆栈发生了这种情况,我们就会被冲洗掉.

但代码获取可能会导致STATUS_IN_PAGE_ERROR,你可以从中恢复.但是不是通过在发生异常的地方恢复执行(除非我们能以某种方式将该页面重新映射到高容错系统中的另一个副本?),所以我们可能仍然可以.

想要读取我们存储在ESP下面的内容的代码中的I/O错误分页排除了读取它的任何机会.如果你不打算这样做,那你很好. 一个不知道这段特定代码的通用SEH处理程序无论如何都不会尝试这样做.我认为通常STATUS_IN_PAGE_ERROR最多会尝试打印错误消息或者记录某些内容,而不是尝试继续进行任何正在进行的计算.

在商店之间访问其他内存并重新加载到下面ESP内存可能会引发STATUS_IN_PAGE_ERROR对于内存.在库代码中,您可能无法假设您传递的其他指针不会很奇怪并且调用者期望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下使用了未知的空间,所以你必须假设每个callclobbers都在堆栈指针下面.因此,这些"异步"回调与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调用约定中使用红区​​时相同.(有关它的更多信息,请参阅https://stackoverflow.com/tags/red-zone/info.)

  • @HadiBrais结构化异常处理程序也可以请求恢复.只需返回`EXCEPTION_CONTINUE_EXECUTION`即可重启失败的指令(大概是在尝试修复问题后). (2认同)
  • Agner 建议系统可能会丢弃低于 ESP 的页面,如果它“内存非常低”。您提到这在当前系统上不会发生。我不知道谁是对的,但我想我会提到它。我找到了引用 [here](https://github.com/tpn/agner/blob/master/objconv/extras/u2wstub.asm#L41) 但也许他在其他地方更多地谈论它。 (2认同)
  • Windows 10 版本 1809 引入了特殊用户 APC,它可以像 UNIX 信号一样随时触发。请参阅 https://repnz.github.io/posts/apc/user-apc/#ntqueueapcthreadex-meet-special-user-apc (2认同)

RbM*_*bMm 7

一般情况下(x86/x64平台) - 中断可以随时执行,它会覆盖内存堆栈指针(如果它在当前堆栈上执行).因为这样,即使临时保存一些下面的堆栈指针,在内核模式下也无效 - 中断将使用当前内核堆栈.但在用户模式情况下,另一个 - 窗口构建中断表(IDT),以便在引发中断时 - 它将始终在内核模式和内核堆栈中执行.结果用户模式堆栈(在堆栈指针下面)将不受影响.并且可能临时使用一些堆栈空间吼它指针,直到你不做任何函数调用.如果异常将是(比方说通过访问无效地址) -也空间波纹管堆栈指针将被覆盖-当然CPU异常开始在内核模式和内核堆栈执行,而不是内核通过在用户空间中执行的回调ntdll.KiDispatchExecption已经在当前的堆栈空间.所以一般来说这在windows用户模式下有效(在当前的实现中),但你需要很好地理解你在做什么.然而,我认为这很少使用


当然,在评论中如何正确指出我们可以在Windows 用户模式下写下堆栈指针 - 这只是当前的实现行为.这没有记录或保证.

但这是非常基础的 - 不太可能会改变:中断总是只在特权内核模式下执行.和内核模式将只使用内核模式堆栈.用户模式上下文根本不受信任.如果用户模式程序设置错误的堆栈指针会是什么?说 mov rsp,1mov esp,1?在此指令之后,将引发中断.如果它开始在这样的无效esp/rsp上执行会是什么?所有的操作系​​统都崩溃了.正是因为这个中断只会在内核堆栈上执行.而不是覆盖用户堆栈空间.

另外需要注意的是堆栈空间有限(即使在用户模式下),在1页(4Kb)之前访问它已经出错(需要逐页进行堆栈探测,移动防护页面向下).

最后真的没有必要通常访问[ESP-4], EAX- 在什么问题ESP首先减少?即使我们需要在循环中访问堆栈空间的大量时间 - 递减堆栈指针只需要一次 - 1个附加指令(不在循环中),性能或代码大小没有任何变化.

所以尽管正式,这将是在Windows用户模式下正确的工作,更好(而不是需要)使用这个


当然正式文件说:

堆栈使用

超出RSP当前地址的所有内存都被视为易失性

但这是常见的情况,包括内核模式.我写了关于用户模式和基于当前实现的文章


可能在将来的窗口中添加"直接"apc或一些"直接"信号 - 一些代码将在线程进入内核之后通过回调执行(在通常的硬件中断期间).在此之后所有下面的esp将是未定义的.但直到不存在.直到此代码始终工作(在当前版本中)正确.


Bre*_*dan 5

一般而言(与任何操作系统无关); 在以下情况下写下ESP是不安全的:

  • 代码可能会被中断,中断处理程序将以相同的权限级别运行.注意:这通常不太可能用于"用户空间"代码,但极可能用于内核代码.

  • 您可以调用任何其他代码(call被调用例程使用的堆栈或垃圾桶可以删除您存储在ESP下面的数据)

  • 其他东西取决于"正常"堆栈使用.这可以包括信号处理,(基于语言)异常展开,调试器,"堆栈粉碎保护器"

如果不是"不安全",可以在ESP下面写一下.

请注意,对于64位代码,在x86-64 ABI("红色区域")内置写入RSP以下; 并通过在工具链/编译器和其他所有方面的支持使其安全.

  • 这个问题是关于Windows的.Linux/MacOS有一个红色区域,因为它是x86-64 System V ABI的一部分. (5认同)