我可以从堆栈中间弹出吗?

Can I pop from the middle of a stack?

在 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,不包括额外的堆栈同步微指令。)

为什么这样做是不好的做法?

您可以直接使用 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

请记住,中断随时可能发生。
中断将假定堆栈指针下方的任何内容都可以更改。如果您手动增加堆栈指针然后访问它下面的数据,就好像它仍在堆栈中一样,您可能会发现那里的数据已经被中断处理程序替换(糟糕)。

规则:ESP以北的任何地方都是安全的,ESP以南的任何地方都被标记为死亡
这就是例程创建 stack frame 的原因。通过降低堆栈指针(记住堆栈向下增长)内存区域受到保护,因为它现在位于堆栈内。
堆栈的语义意味着任何高于 ESP 的数据都是安全的,任何低于 ESP 的数据都是公平的。

如果您违反了这两个原则中的任何一个
A - 使用非固定 ESP 作为基指针,或
B - 访问低于 ESP 的数据。
您将面临 A:破坏他人数据或 B:自己处理损坏数据的风险。

Is this bad practise?

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 之前发生中断,则存储在该位堆栈 space 中的 3 个整数将被更改,从而导致您的应用程序中的数据损坏。

改用以下代码。

mov eax,[esp+8]

此代码 A:安全,B:更快,C:不会破坏标志寄存器,D:更短,E:编码字节更少。

关于add/sub
的注释 如果您在 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 个或更多其他指令时,一定要使用 LEA,但不仅仅是替换 add/sub 除非你有保留 FLAGS 的用途。

So why would'nt i work with the stack pointer directly?

EBP 用作帧指针,以便更容易编写调试器(或者,调试器更容易找出当前堆栈帧并确定局部变量和参数在哪里)。

EBP 用作帧指针会使它无法用于任何其他用途,这对性能不利 - 可以使用的寄存器越少意味着移动临时值 to/from 堆栈所花费的时间越多。好的代码(和好的调试器)不会使用或需要 EBP 作为帧指针。好的编译器通常支持 omit/disable 使用 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