当需要额外的堆栈对齐时,gcc奇怪的堆栈操作有什么用?

时间:2017-07-31 18:55:04

标签: gcc assembly x86 compiler-optimization

我已经看过几次r10这种奇怪的事了,所以让我们看看是否有人知道这些是什么。

采取这个简单的功能:

#define SZ 4

void sink(uint64_t *p);

void andpop(const uint64_t* a) {
    uint64_t result[SZ];
    for (unsigned i = 0; i < SZ; i++) {
        result[i] = a[i] + 1;
    }

    sink(result);
}

它只是向传入数组的4个64位元素中的每一个添加1并将其存储在本地并在结果上调用sink()(以避免整个函数被优化掉)。 / p>

这是corresponding汇编:

andpop(unsigned long const*):
        lea     r10, [rsp+8]
        and     rsp, -32
        push    QWORD PTR [r10-8]
        push    rbp
        mov     rbp, rsp
        push    r10
        sub     rsp, 40
        vmovdqa ymm0, YMMWORD PTR .LC0[rip]
        vpaddq  ymm0, ymm0, YMMWORD PTR [rdi]
        lea     rdi, [rbp-48]
        vmovdqa YMMWORD PTR [rbp-48], ymm0
        vzeroupper
        call    sink(unsigned long*)
        add     rsp, 40
        pop     r10
        pop     rbp
        lea     rsp, [r10-8]
        ret

很难理解r10几乎发生的一切。首先,r10设置为指向rsp + 8,然后指向push QWORD PTR [r10-8],据我所知,它会在堆栈上推送返回地址的副本。然后,rbp正常设置,最后r10本身被推送。

要展开这一切,r10会从堆栈中弹出,并用于将rsp恢复为原始值。

一些观察结果:

  • 查看整个函数,所有这些似乎都是在rsp之前简单地将ret恢复为原始值的完全迂回方式 - 但通常是{{1}的结尾也会这样做(见mov rsp, rpb)!
  • 那就是说,(昂贵的)clang在这项任务中甚至没有帮助:这个值(返回地址?)显然从未使用过。
  • 为什么push QWORD PTR [r10-8]被推挤并弹出?该值在非常小的函数体中没有被破坏,并且没有套准压力。

该怎么办?我之前已经多次看过它,它通常想要r10,有时候r10。看起来很可能与将堆栈对齐到32个字节有关,因为如果将r13更改为小于4,则使用SZ操作,问题就会消失。

以下xmm例如:

SZ == 2

好多了!

1 个答案:

答案 0 :(得分:3)

嗯,你回答了你的问题:在使用对齐的AVX2加载和存储访问堆栈指针之前,需要将堆栈指针对齐到32个字节,但ABI只提供16字节对齐。由于编译器无法知道对齐关闭多少,因此必须将堆栈指针保存在暂存寄存器中,然后将其恢复。但是保存的值必须比函数调用更长,因此必须将其放在堆栈上,并且必须创建堆栈帧。

某些x86-64 ABI有一个红色区域(堆栈指针下方的堆栈区域,信号处理程序不使用),因此,对于这样的短函数,根本不更改堆栈指针是可行的,但是GCC显然没有实现这种优化,但由于最后的函数调用,它无论如何都不适用。

此外,默认的堆栈对齐实现相当差。对于这种情况,-maccumulate-outgoing-args使用GCC 6产生更好看的代码,只是在保存RBP后对齐RSP,而不是在保存RBP之前复制返回地址:

andpop:
        pushq   %rbp
        movq    %rsp, %rbp            # make a traditional stack frame
        andq    $-32, %rsp            # reserve 0 or 16 bytes
        subq    $32, %rsp

        vmovdqu (%rdi), %xmm0         # split unaligned load from tune=generic
        vinserti128     $0x1, 16(%rdi), %ymm0, %ymm0   # use -march=haswell instead
        movq    %rsp, %rdi
        vpaddq  .LC0(%rip), %ymm0, %ymm0
        vmovdqa %ymm0, (%rsp)

        vzeroupper
        call    sink@PLT
        leave
        ret

(编者注:gcc8及更高版本默认使用asm(Godbolt compiler explorer with gcc8, clang7, ICC19, and MSVC),即使没有-maccumulate-outgoing-args

当我们不得不为GCC __tls_get_addr ABI bug实现变通方法时,最近出现了这个问题(GCC生成的堆栈对齐代码很差),最后我们手工编写了堆栈重组。

编辑还有另一个与RTL传递顺序相关的问题:在最终确定是否实际需要堆栈之前选择堆栈对齐,as BeeOnRope's second example shows