6

在 x86 汇编语言中:

我假设我有一个正常的功能序言,请阅读

push ebp
mov  ebp,esp

我知道我可以通过访问内存目标操作数来读取或写入寄存器,假设我想要第一个参数。我会做

mov eax,[ebp +8]

从堆栈中获取 fe 一个整数参数。

那么为什么我不直接使用堆栈指针呢?

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。/esp,imm8sub是3字节,pop eax是1字节。
mov eax, [esp+8]是4字节指令:与16位寻址模式不同,ESP可以是基址寄存器。但它确实需要一个 SIB 字节来对其进行编码。
这些都是现代 CPU 上的单微指令,不包括额外的堆栈同步微指令。)

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

4

2 回答 2

5

您可以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:以更少的字节编码。

关于 add/sub 的说明
如果你在 FLAGS 中有一些有用的东西,你可以通过使用LEAfor add 来避免破坏它。如果不是,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 个或更多其他指令时,一定要使用它,但不仅仅是替换添加/子指令,除非您有保留 FLAGS 的用途。

于 2017-03-24T08:01:15.043 回答
3

那么我为什么不直接使用堆栈指针呢?

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
于 2017-03-24T10:13:37.163 回答