在不使用弹出操作的情况下读取数据是否有优势?

时间:2019-04-05 12:07:16

标签: linux assembly x86 stack nasm

根据this PDF document (Page-66),以下几条语句

mov eax, DWORD PTR SS:[esp]
mov eax, DWORD PTR SS:[esp + 4]
mov eax, DWORD PTR SS:[esp + 8]

等同于以下语句:

pop eax
pop eax
pop eax

前者比后者有什么优势吗?

2 个答案:

答案 0 :(得分:1)

mov将数据保留在堆栈上,pop将其删除,因此您只能按顺序读取一次。除非您使用的调用约定/ ABI在堆栈指针下方包含红色区域,否则ESP以下的数据必须被视为“丢失”。

数据通常 仍在ESP下方,但是可以使用异步信号(例如信号处理程序或在过程上下文中评估call fflush(0)的调试器)来

此外,pop修改了ESP,因此每个pop都需要在可执行文件/库的另一部分中展开堆栈元数据 1 ,因为它完全符合ABI要求Windows上的SEH或其他操作系统上的i386 / x86-64 System V ABI(指定所有功能都需要展开元数据,即使它们不是实际上支持传播异常的C ++功能)也是如此。


但是,如果您是最后一次读取数据,并且确实需要全部数据,那么然后yes pop是一种在现代CPU上读取数据的有效方法(例如Pentium-M和更高版本, a stack engine to handle the ESP updates而没有单独的uop。)

在奔腾III等较旧的CPU上,pop实际上比3倍的mov + add esp,12慢,并且编译器确实按照Brendan的答案所示的方式生成代码。

void foo() {
    asm("" ::: "ebx", "esi", "edi");
}

此函数强制编译器保存/恢复3个保留调用的寄存器(通过在它们上声明Clobbers)。它实际上并不是真正地触摸它们。内联asm字符串为空。但是,这很容易看出编译器将如何进行保存/还原。 (这是他们唯一一次正常使用pop的时间。)

GCC的默认(tune = generic)代码生成,或者例如使用-march=skylake,就像这样(from the Godbolt compiler explorer

foo:                        # gcc8.3 -O3 -m32
        push    edi
        push    esi
        push    ebx
        pop     ebx
        pop     esi
        pop     edi
        ret

但是告诉它调整没有堆栈引擎的旧CPU可以做到这一点:

foo:                     # gcc8.3  -march=pentium3 -O3 -m32
        sub     esp, 12
        mov     DWORD PTR [esp], ebx
        mov     DWORD PTR [esp+4], esi
        mov     DWORD PTR [esp+8], edi
        mov     ebx, DWORD PTR [esp]
        mov     esi, DWORD PTR [esp+4]
        mov     edi, DWORD PTR [esp+8]
        add     esp, 12
        ret

gcc认为-march=pentium-m没有堆栈引擎,或者至少选择不使用push/pop。我认为这是一个错误,因为Agner Fog's microarch pdf确实将堆栈引擎描述为Pentium-M中的版本。

在PM及更高版本上,push / pop是单联指令,ESP更新是在无序后端之外进行处理的,而对于push来说,存储地址+存储数据是微融合的。

在Pentium 3上,它们分别为2或3 uops。 (再次,请参阅Agner Fog的说明表。)

在有序P5 Pentium上,pushpop实际上很好。 (但是通常避免使用诸如add [mem], reg之类的内存目标指令,因为P5不会将它们拆分为微指令以更好地进行流水线操作。)


在现代Intel CPU上,将pop直接引用到[esp]的混合实际上可能比仅使用其中一个慢,因为这会花费额外的堆栈同步指令。


显然,EAX背靠背写了3次,这意味着前两个负载在两个序列中都没有用。

请参阅Extreme Fibonacci,了解pop(1 uop或类似1.1 uop,并已摊销堆栈同步uops)的示例比lodsd(Skylake上为2 uops)读取数组的效率更高。 (在邪恶的代码中假设一个大的红色区域,因为它没有安装信号处理程序。除非您确切地知道自己在做什么以及何时中断,否则请不要这样做;这更多是愚蠢的计算机技巧/对代码高尔夫球进行了极端的优化,而不是实际有用的任何方法。)


脚注1:通常,Godbolt编译器资源管理器会过滤掉多余的汇编器指令,但是如果您取消选中该框,则可以看到在每次push / pop之后,使用push / pop的gcc函数具有.cfi_def_cfa_offset 12弹出。

        pop     ebx
        .cfi_restore 3
        .cfi_def_cfa_offset 12
        pop     esi
        .cfi_restore 6
        .cfi_def_cfa_offset 8
        pop     edi
        .cfi_restore 7
        .cfi_def_cfa_offset 4

.cfi_restore 7元数据伪指令必须存在,而与push / pop和mov无关,因为这可以使堆栈展开时在展开时恢复调用保留的寄存器。 (7是寄存器号。

但是对于函数内部的push / pop的其他用法(例如将args推入函数调用,或使用伪pop将其从堆栈中删除),您将没有.cfi_restore,只是堆栈指针的元数据相对于堆栈框架发生了变化。

通常,您不必担心手写asm的问题,但是编译器必须正确解决此问题,因此就可执行文件的总大小而言,使用push/pop会产生少量额外费用。但是,仅在文件中通常未映射到内存中且未与代码混合的部分中。

答案 1 :(得分:0)

此:

pop eax
pop ebx
pop ecx

..等效于此:

mov eax,[esp]
add esp,4

mov ebx,[esp]
add esp,4

mov ecx,[esp]
add esp,4

..它可能像这样:

mov eax,[esp]     ;Do this instruction
add esp,4         ; ...and this instruction in parallel

                   ;Stall until the previous instruction completes (and the value
mov ebx,[esp]      ;in ESP becomes known); then do this instruction
add esp,4          ; ...and this instruction in parallel

                   ;Stall until the previous instruction completes (and the value
mov ecx,[esp]      ;in ESP becomes known); then do this instruction
add esp,4          ; ...and this instruction in parallel

对于此代码:

mov eax, [esp]
mov ebx, [esp + 4]
mov ecx, [esp + 8]
add esp,12

..所有指令都可以并行发生(理论上)。

注意:实际上,以上所有内容取决于哪个CPU等。