我可以从堆栈中间弹出吗?
Posted
技术标签:
【中文标题】我可以从堆栈中间弹出吗?【英文标题】:Can I pop from the middle of a stack? 【发布时间】:2017-08-16 22:07:27 【问题描述】:在 x86 汇编语言中:
我假设我有一个正常的功能序言,阅读
push ebp
mov ebp,esp
我知道我可以通过访问内存目标操作数来读取或写入寄存器,假设我想要第一个参数。我会做的
mov eax,[ebp +8]
得到 f.e.堆栈中的整数参数。
那么我为什么不直接使用堆栈指针呢?
add esp,8 ; point ESP at the data we want
pop eax
sub esp,12 ; restore ESP to its original position
这会导致错误吗?这在任何情况下都使用吗?
我当然知道第一个操作的大小更小,因为它只有一个操作码,即mov
,而不是三个,但这不是问题的重点。
(编者注:mov eax, [ebp+8]
是 x86 机器码中的 3 字节指令。add
/sub
esp,imm8 每个是 3 个字节,pop eax
是 1 个字节。mov eax, [esp+8]
是4 字节指令:与 16 位寻址模式不同,ESP 可以是基址寄存器。但它确实需要一个 SIB 字节来对其进行编码。
这些都是single-uop instructions on modern CPU,不包括额外的堆栈同步微指令。)
为什么这样做是不好的做法?
【问题讨论】:
我假设您的意思是add
而不是 inc
和 sub
而不是 dec
。与此相关的一个问题是,如果您在打开中断的情况下运行并且在pop eax
和sub esp,12
之间发生中断,那么低于 ESP 的任何内容都可能被破坏(中断将导致数据被推入堆栈)。出于同样的原因,inc esp, 8
和 pop eax
之间的中断将成为潜在问题。如果您仍然需要将数据保留在 ESP 之下,那么这将是一个问题。不知道你在dec esp,12
之后如何使用堆栈数据,所以很难说这是否会成为问题。
【参考方案1】:
您可以直接将ESP
用作指针。
但是,如果发生任何推动或弹出,那么 ESP 就会变成一个移动的目标,使您的计算变得更加困难。
出于这个原因,我们将堆栈指针的副本放在 EBP 中,这样我们就不必担心 ESP 的变化。
但是,如果您不打算更改堆栈指针,那么使用ESP
代替EBP
是完全可以的。
如果您确实更改了ESP
,您当然可以相应地更改 ESP 的偏移量。
警告 你不应该这样做:
add esp,8
mov ecx,[esp-4] //never access data outside the actual stack.
pop eax
sub esp,12
请记住,中断随时可能发生。 中断将假定堆栈指针下方的任何内容都可以更改。如果您手动增加堆栈指针,然后访问它下面的数据,就好像它仍在堆栈中一样,您可能会发现那里的数据已经被中断处理程序替换(Oops)。
规则:ESP 以北的任何东西都是安全的,ESP 以南的任何东西都被标记为死亡
这就是例程创建stack frame
的原因。通过降低堆栈指针(记住堆栈向下增长),内存区域受到保护,因为它现在位于堆栈内。
堆栈的语义意味着任何高于 ESP 的数据都是安全的,任何低于 ESP 的数据都是公平的游戏。
如果您违反了这两个原则中的任何一个 A - 使用非固定 ESP 作为基指针,或 B - 访问低于 ESP 的数据。 您将面临 A:损坏其他人的数据或 B:自己处理损坏的数据的风险。
这是不好的做法吗?
add esp,8 //equivalent to pop anyreg, pop anyreg pop eax //pop from the (new) top of the stack. sub esp,12 //reset the stack back to where is was.
是的!这很糟糕
如果在sub esp,12
之前发生中断,则存储在此堆栈空间位中的 3 个整数将被更改,从而导致您的应用程序中的数据损坏。
请改用以下代码。
mov eax,[esp+8]
此代码是 A:安全,B:更快,C:不破坏标志寄存器,D:更短,E:以更少的字节编码。
关于添加/订阅的说明
如果你在 FLAGS 中有一些有用的东西,你可以通过使用 LEA
来避免破坏它。如果没有,add
/sub
至少也一样快(例如,在某些主流 CPU 上运行在更多执行端口上,而 LEA 只能在 Ryzen 和 Haswell 及更高版本的 4 个整数 ALU 执行单元中的 2 个上运行)。两种方式都没有代码大小的优势。
lea esp,[esp+8] == add esp,8 (but without altering the flags).
lea edx, [esp+8] ; copy-and-add replacing mov + add is very useful
当 LEA 可以替换 2 个或更多其他指令时,一定要使用它,但不能仅仅替换 add/sub,除非您有保留 FLAGS 的用途。
【讨论】:
对于用户空间(而不是内核代码),任何“ESP 以南”的东西都可以是完全安全的——当 IRQ 发生时,CPU 会切换到不同的堆栈。 EBP 主要用作“帧指针”,以便更容易编写能够计算堆栈帧的调试器;并且(不包括某些调试器)最好不要将 EBP 用作“帧指针”并将其仅用作另一个通用寄存器(特别是对于通用寄存器数量非常有限的 32 位 80x86)。 @Brendan 我不认为 OP 确实指定了目标平台,所以如果他正在运行一些奇怪的 32b 模式设置(例如他自己的自定义操作系统),他可能有共享堆栈。 ;) IMO 以这种经典的方式先学习它们并没有什么坏处,然后再介绍常见的 x86 怪癖和便利性,原则,他们可能稍后会在其他平台上遇到问题。 @Ped7g:我更喜欢涵盖所有可能性,并发现那些被教导像编译器一样编写汇编的人很少从“汇编必须受到完全不同语言的限制”的初始状态中恢复过来假设。另请注意,“RSP 以南”的使用已成为至少一种现代调用约定(AMD64 ABI 的“红色区域”)的官方部分。【参考方案2】:那么我为什么不直接使用堆栈指针呢?
EBP
用作帧指针,以便更容易编写调试器(或者,调试器更容易找出当前堆栈帧并确定局部变量和参数的位置)。
使用EBP
作为帧指针会使其无法用于其他任何操作,这对性能不利 - 可以使用的寄存器越少,意味着将临时值移入/移出堆栈所花费的时间越多。好的代码(和好的调试器)不使用或不需要EBP
作为帧指针。好的编译器通常支持忽略/禁用 EBP
作为帧指针的选项(例如 GCC 中的“--fomit-frame-pointer”)。
这段代码效率低:
add esp,8
pop eax
sub esp,12
但是,根据代码的使用方式,它可能非常危险或完全安全。更具体地说,它取决于代码是否必须迎合异步事件(信号、IRQ),这些事件假定数据可以被推送到堆栈上而不会破坏堆栈上的现有数据。
更好的是:
mov eax,[esp+8]
这样更有效率,而且总是安全的。
注意:在某些(相对极端的)条件下,将 ESP 用作另一个通用寄存器也是完全安全的。对于一个简单的场景,考虑这个:
;Copy array somewhere else, while reversing the order of elements
;
;Input
; ecx Number of elements in array
; esi Address of source array
; edi Address of destination array
reverseArray:
mov ebx,esp ;ebx = stack top
lea esp,[edi+ecx*4] ;esp = address of byte after array
cld
.next:
lodsd ;eax = next element in source array
push eax ;Store it in destination array
loop .next ;Do all elements
mov esp,ebx ;Restore stack
ret
【讨论】:
我知道编译器(如上面提到的 gcc)不使用 ebp 会更有效,但是当手动编写汇编代码时,我想找出所有地址要困难得多。 @clockw0rk 在您手动编写 asm 时,通常出于性能原因这样做.. 那时您会尽可能避免使用堆栈(以及任何堆栈调用约定),因此您只需要解决这个问题很少,例如当您连接某个 C 库或其他东西时,您无法强制执行自己的调用约定而忽略堆栈。 @clockw0rk:当手工编写汇编时,您可能会在例程开始附近的一个漂亮有序的组中添加一堆定义(例如%define .myLocalVar1 [esp+8]
)(这并不比找出偏移量更难) EBP
)。如果您在例行程序中间推动/弹出,它只会变得有点混乱。但是,通过在寄存器中传递参数和小心使用寄存器(以避免堆栈上的局部变量),您通常根本不需要堆栈上任何东西的地址。以上是关于我可以从堆栈中间弹出吗?的主要内容,如果未能解决你的问题,请参考以下文章
boost::lockfree::spsc_queue 忙等待策略。有阻塞弹出吗?