下面的ESP是否有效?

2022-01-27 04:28:37 标签 windowsassemblyx86abistack-pointerred-zone

对于一个32位的windows应用程序,它是否有效使用堆栈内存低于ESP临时交换空间而不显式递减ESP?

假设有一个函数在ST(0)中返回一个浮点值。例如,如果我们的值当前在EAX中,我们会这样做

PUSH   EAX
FLD    [ESP]
ADD    ESP,4  // or POP EAX, etc
// return...

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

MOV    [ESP-4], EAX
FLD    [ESP-4]
// return...

在这两种情况下,发生的事情是一样的,除了在第一种情况下,我们注意在使用内存之前减少堆栈指针,然后在使用内存之后增加它。在后一种情况下,我们不需要。

尽管确实需要在堆栈上持久化这个值(在push和读回值之间,重入会发出函数调用等),但是为什么像这样写入ESP下面的堆栈是无效的呢?

###TL:DR:不,有一些SEH的角落案例可以使它在实践中不安全,并被记录为不安全。@Raymond Chen最近写了一篇博客文章,你可能应该看看,而不是这个答案。

他举了一个代码获取页面错误I/O错误的例子,这个错误可以通过提示用户插入一个CD-ROM并重试来“修复”,这也是我得出的结论,如果在ESP/RSP下面的存储和重新加载之间没有任何其他可能的错误指令,那么这个错误实际上是可以恢复的。

或者,如果你让调试器调用正在调试的程序中的函数,它也会使用目标进程的堆栈。

这个答案列出了一些你认为可能会在ESP下面占用内存但实际上不会占用的东西,这可能会很有趣。在实践中,似乎只有SEH和调试器可能是一个问题。

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

但是,在堆栈上有一些其他的用例,在这些用例中,可以节省一些用于偏移ESP/RSP的指令。

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

它被明确记录为不安全的微软:(对于64位代码,我没有找到一个等价的语句,32位代码,但我确信有一个)

堆栈使用(适用于x64)

RSP当前地址之外的所有内存都被认为是易失的:操作系统或调试器可能会在用户调试会话或中断处理程序期间覆盖这个内存。

这就是文档,但是所述的中断原因对用户空间堆栈和内核堆栈没有意义。重要的部分是,他们的文件不保证安全,而不是给出的理由。

硬件中断不能使用用户堆栈;这将让用户空间进程中的另一个线程在中断处理程序运行时修改返回地址,从而使内核崩溃。这就是为什么内核总是把中断上下文配置到内核堆栈上。

现代的调试器运行在一个独立的进程中,没有“侵入性”。在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(结构化异常处理)让用户空间软件有一些硬件异常,比如除0,类似于c++的异常。它们并不是真正的异步:它们是针对由您运行的指令触发的异常,而不是针对一些随机指令之后发生的事件。

但与正常异常不同的是,SEH处理程序可以做的一件事是从异常发生的地方恢复。(@RossRidge注释:SEH处理程序最初是在展开堆栈的上下文中调用的,可以选择忽略异常,并在异常发生的地方继续执行。)

因此,即使当前函数中没有catch()子句,这也是一个问题。

正常情况下,HW异常只能同步触发。例如,div指令或内存访问可能与STATUS_ACCESS_VIOLATION (Windows相当于Linux的SIGSEGV分割错误)故障。你可以控制你使用的指令,这样你就可以避免那些可能出错的指令。

如果你限制你的代码在存储和重新加载之间只访问堆栈内存,并且你尊重堆栈增长保护页面,你的程序不会因为访问[esp-4]而出错。(除非你达到最大堆栈大小(stack Overflow),在这种情况下push eax也会出错,你不能真正从这种情况中恢复,因为没有堆栈空间给SEH使用。)

所以我们可以排除status_access_breach作为一个问题,因为如果我们在访问堆栈内存时得到它,我们无论如何都会被占用。

STATUS_IN_PAGE_ERROR的SEH处理程序可以在任何加载指令之前运行。Windows可以换出它想要的任何页面,并在再次需要时透明地换回它(虚拟内存分页)。但是如果有一个I/O错误,你的Windows试图让你的进程通过提交一个STATUS_IN_PAGE_ERROR来处理失败

如果这发生在当前堆栈上,我们就完蛋了。

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

在想要读取我们存储在ESP下面的内容的代码中,I/O错误分页排除了任何读取它的机会。如果你本来就不打算这么做你也没事。不知道这段特定代码的通用SEH处理程序无论如何也不会尝试这样做。我想通常STATUS_IN_PAGE_ERROR最多会打印一个错误消息或者记录一些东西,而不是尝试进行任何正在发生的计算。

在store和重新加载到ESP下面的内存之间访问其他内存可能会触发该内存的STATUS_IN_PAGE_ERROR。在库代码中,你可能不能假设你传递的其他指针不会奇怪,并且调用者期望为它处理STATUS_ACCESS_VIOLATION或PAGE_ERROR。

当前的编译器不会利用ESP/RSP下面的空间在Windows上,即使他们确实利用x86-64 System V中的红色区域(在叶函数中,需要溢出/重新加载一些东西,就像你正在做的int ->x87)。这是因为MS说这是不安全的,他们不知道是否存在可以在SEH之后恢复的SEH处理程序。

你认为在当前的Windows系统中可能会出现问题的东西以及它们不会出现问题的原因:

ESP下面的保护页面内容:只要你不超出当前ESP的范围,你就会接触到保护页面,并触发分配更多的堆栈空间,而不是出现故障。只要内核没有检查用户空间ESP,并发现您正在接触堆栈空间,而没有首先“保留”它,那么这是没问题的。

ESP/RSP下面的内核页面回收:显然Windows目前没有这样做。因此,一旦使用大量的堆栈空间,就会在余下的进程生命周期中保持这些页面的分配,除非您手动使用VirtualAlloc(MEM_RESET)来分配它们。(内核可以这样做,因为文档说RSP下面的内存是易失的。如果内核希望在写时复制将其映射到一个零页,而不是在内存压力下将其写入页面文件,那么内核可以有效地异步地将其归零。)

APC(异步过程调用):它们只能在进程处于“alertable状态”的时候被发送,这意味着只有在调用一个函数的时候,比如slepex(01)。调用函数已经使用了E/RSP下面未知数量的空间,所以你必须假设每次调用都会破坏堆栈指针下面的所有内容。因此,这些“异步”回调与Unix信号处理程序的正常执行方式相比,并不是真正的异步。(有趣的事实:POSIX异步io确实使用信号处理程序来运行回调)。

控制台应用程序对ctrl-C和其他事件的回调(SetConsoleCtrlHandler)。这看起来就像注册一个Unix信号处理程序,但在Windows中,该处理程序运行在一个独立的线程中,具有自己的堆栈。(见RbMm评论)

SetThreadContext:当这个线程挂起的时候,另一个线程可以异步地改变我们的EIP/RIP,但是整个程序必须为此专门编写,才有意义。除非是调试器在使用它。当其他线程干扰您的EIP时,正确性通常是不需要的,除非环境非常受控制。

显然,没有其他方法可以让另一个进程(或者这个线程注册的东西)触发任何与Windows上用户空间代码的执行相比的异步执行。

如果没有SEH处理程序,可以尝试恢复Windows,或多或少有一个4096字节的红色区域低于ESP(或可能更多,如果您增量接触它?),但RbMm说,没有人在实践中利用它。这并不奇怪,因为MS说不要这样做,而且你不能总是知道你的呼叫者是否对SEH做了什么。

显然,任何可能会同步地破坏它的东西(比如调用)也必须再次避免,就像在x86-64 System V调用约定中使用红色区域时一样。(详见https://stackoverflow。com/tags/red-zone/info了解更多信息。)

###通常情况下(x86/x64平台)-中断可以在任何覆盖内存低于堆栈指针(如果它在当前堆栈上执行)的时间执行。因为这个即使是堆栈指针下面的临时保存在内核模式下也无效- interrupt将使用当前内核堆栈。但是在用户模式下,另一种情况——windows以这样的方式构建中断表(IDT),当中断被触发时——它将始终在内核模式和内核堆栈中执行。因此,用户模式堆栈(在堆栈指针下方)将不受影响。可能的临时使用一些堆栈空间,直到你不做任何函数调用。如果异常将(例如访问无效地址)-也空间下面的堆栈指针将被覆盖- CPU异常当然开始在内核模式和内核堆栈执行,但内核执行回调通过用户空间ntdll.KiDispatchExecption already on current stack space. so in general this is valid in windows user mode (in current implementation) but you need good understand what you doing. however this is very rarely i think used

已经在当前堆栈空间上。所以通常这在Windows用户模式下是有效的(在当前实现中),但你需要很好地理解你在做什么。然而,我认为这是很少使用的

当然,在Windows用户模式下,我们在堆栈指针下面写注释的正确程度只是当前的实现行为。这没有记录或保证。

但这是非常基本的——不太可能改变:中断总是只在特权内核模式下执行。而内核模式将只使用内核模式堆栈。用户模式上下文根本不受信任。如果用户模式程序设置了错误的堆栈指针会是什么?说的

Mov rsp1还是Mov esp1 ?在这个指令之后,中断就会被触发。如果它开始在这样无效的esp/rsp上执行,会是什么?所有操作系统都崩溃了。正是因为这个中断将只在内核堆栈上执行。并且不覆盖用户堆栈空间。

还需要注意的是堆栈是有限的空间(即使在用户模式下)访问它在1页(4Kb)以下已经错误(需要做堆栈探测逐页移动保护页面)。

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

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

当然,正式文件是这样写的:

堆栈的使用

RSP当前地址以外的所有内存都被认为是易失的

但这也适用于包括内核模式的常见情况。我写了关于用户模式和基于当前的实现

可能在未来的Windows中添加“直接”apc或一些“直接”信号——一些代码将通过回调在线程进入内核后执行(在通常的硬件中断期间)。在此之后,esp将被定义。但在此之前。直到此代码将始终工作(在当前构建中)正确。

###一般情况下(不涉及任何操作系统);下面的ESP是不安全的,如果:

代码可能被中断,而中断处理程序将在相同的特权级别上运行。注意:对于“用户空间”代码来说,这通常是非常不可能的,但是对于内核代码来说,这是非常可能的。

您调用任何其他代码(其中调用或被调用例程使用的堆栈都可以丢弃存储在ESP下面的数据)

还有一些东西依赖于“正常的”堆栈使用。这可以包括信号处理(基于语言)异常解除调试器“堆栈粉碎保护”。

如果它不是“不安全”,那么在ESP下面写是安全的。

请注意,在RSP下面编写的64位代码内置在x86-64 ABI中(“红色区域”);并且通过在工具链/编译器和其他一切工具中对它的支持而变得安全。

当一个线程被创建时,Windows会为线程的堆栈保留一个可配置大小的连续虚拟内存区域(默认为1mb)。最初堆栈看起来是这样的(堆栈向下增长):

--------------
|  committed |
--------------
| guard page |
--------------
|     .      |
| reserved   |
|     .      |
|     .      |
|            |
--------------

ESP将指向提交页面中的某个位置。保护页面用于支持堆叠自动增长。保留页区域确保请求的堆栈大小在虚拟内存中可用。

考虑问题中的两个说明:

MOV    [ESP-4], EAX
FLD    [ESP-4]

有三种可能:

第一个指令成功执行。没有任何使用用户模式堆栈的东西可以在这两条指令之间执行。因此,第二条指令将使用正确的值(@RbMm在他的答案下面的评论中声明了这一点,我同意)。

第一条指令引发异常,异常处理程序没有返回EXCEPTION_CONTINUE_EXECUTION. As long as the second instruction is immediately after the first one (it is not i. 只要第二条指令紧跟在第一条指令之后(它不在异常处理程序中或放在它的后面),那么第二条指令就不会执行。所以你还是安全的。从存在异常处理程序的堆栈帧开始继续执行。

第一条指令引发一个异常,一个异常处理程序返回EXCEPTION_CONTINUE_EXECUTION. Execution continues from the same instruction that raised the exception (. 从引发异常的同一条指令继续执行(可能带有被处理程序修改的上下文)。在这个特殊的例子中,第一个将被重新执行,以写入下面的值ESP. No problem. If the second instruction raised an exception or there are more than two instructions then the exception might occur a place after a value is written below ESP. When the exception h. 没有问题。如果第二条指令引发了一个异常,或者有两条以上的指令,那么异常可能发生在下面写入值之后的位置ESP. When the exception handler gets called it may overwrite the value and then return EXCEPTION_CONTINUE_EXECUTION. But when execution resumes the value writte. 当异常处理程序被调用时,它可能会覆盖该值,然后返回EXCEPTION_CONTINUE_EXECUTION. But when execution resumes the value written is assumed to still be there but it's. 但当执行继续时,写入的值被认为仍然在那里,但它已经不在了。在这种情况下,在下面写是不安全的ESP. This applies even if all of the instructions are placed consecutively. Thanks to @RaymondChen for pointing this out.. 即使所有指令都是连续放置的,这也适用。感谢@RaymondChen指出这一点。

一般来说,如果这两条指令不是背靠背放置的,如果您正在写入ESP之外的位置,则不能保证写入的值不会被损坏或覆盖。我能想到的一个可能发生这种情况的例子是结构化异常处理(SEH)。如果硬件定义的异常(比如除以0)发生,内核异常处理程序将在内核模式下被调用(KiUserExceptionDispatcher),内核模式将调用该处理程序的用户模式端(RtlDispatchException)。当从用户模式切换到内核模式,然后再切换到用户模式时,ESP中的任何值都将被保存和恢复。然而,用户模式处理程序本身使用用户模式堆栈,并将迭代已注册的异常处理程序列表,每个异常处理程序都使用用户模式堆栈。这些函数将根据需要修改ESP。这可能会导致您在ESP之外写入的值丢失。使用软件定义异常(在vc++中抛出)时也会出现类似的情况。

我认为你可以通过在任何其他异常处理程序之前注册你自己的异常处理程序来处理这个问题(这样它就会首先被调用)。当您的处理程序被调用时,您可以将数据保存到ESP之外的其他地方。稍后在展开过程中,您将有清理的机会将数据恢复到堆栈上的相同位置(或任何其他位置)。

您还需要类似地注意异步过程调用(apc)和回调。

###这里有几个答案提到APCs(异步过程调用),它们只能在进程处于“alertable状态”的时候被发送,并且它们与Unix信号处理程序的正常执行方式相比不是真正的异步的

Windows 10 1809版引入了特殊用户apc,可以像Unix信号一样随时发射。有关底层细节,请参阅本文。

特殊用户APC是在RS5中添加的一种机制(并通过NtQueueApcThreadEx暴露),但最近(在内部构建中)通过一个新的系统调用暴露- NtQueueApcThreadEx2。如果使用这种类型的APC,线程会在执行中间发出信号来执行特殊的APC。

阅读全文

▼ 版权说明

相关文章也很精彩
推荐内容
更多标签
相关热门
全站排行
随便看看

错说 cuoshuo.com —— 程序员的报错记录

部分内容根据CC版权协议转载;网站内容仅供参考,生产环境使用务必查阅官方文档

辽ICP备19011660号-5

×

扫码关注公众号:职场神器
发送: 1
获取永久解锁本站全部文章的验证码