为什么生成的数组分配和访问GCC代码不遵循`movl(%rdx,%rcx,4),%eax`公式?

mbzjlibv  于 2023-04-06  发布在  其他
关注(0)|答案(1)|浏览(104)

我正在考虑如何在x86-64汇编中编译数组。
我正在阅读“计算机系统-程序员的观点”,作者给予了以下公式:

E[i] ->  `movl (%rdx, %rcx, 4i), %eax`

但我只是使用编译器资源管理器和GCC 12.2来查看以下程序的生成程序集:

int main() {
    int arr[] = {3,4,5};
    for (int i = 0; i < 3; i++) {
        arr[i];

    }
}

为此生成的代码是:

main:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-16], 3
        mov     DWORD PTR [rbp-12], 4
        mov     DWORD PTR [rbp-8], 5
        mov     DWORD PTR [rbp-4], 0
        jmp     .L2
.L3:
        add     DWORD PTR [rbp-4], 1
.L2:
        cmp     DWORD PTR [rbp-4], 2
        jle     .L3
        mov     eax, 0
        pop     rbp
        ret

下面引用a stackoverflow answer的一段话,解释寄存器RBP和RSP的作用:
rbp是x86_64上的帧指针。在生成的代码中,它会获取堆栈指针(rsp)的快照,以便在对rsp进行调整(即为局部变量保留空间或将值推送到堆栈上)时,仍然可以从rbp的常量偏移量访问局部变量和函数参数。
RBP 16在这里吗?例如rbp - 16 = 0rbp - 12 = 4rbp - 8 = 8哪种满足公式。
无论如何,你能给予我一个概念,教科书中的公式是如何与GCC生成的代码相关的吗?

llew8vvj

llew8vvj1#

因为arr不是volatile,所以arr[i];作为C语句编译为零指令,即使在调试版本中也是如此。您看到的所有asm只是固定位置的局部变量。
如果您习惯于查看AT&T语法,请使用Godbolt上的输出下拉列表取消选中“Intel Syntax”。
此外,您可以通过编写一个函数来获取有用的asm,该函数接受指针arg并对其求和,例如因此,您可以启用轻度优化,而无需将数组优化为仅返回一个常量。如果您真的希望看到将数组初始值设定项存储到堆栈中的asm,而不是仅使用函数arg,则可以使用volatile强制它不返回常量。我们编写这个代码是为了查看asm,而不是运行它,所以不要编写main()。(一般情况下,请参见 * How to remove "noise" from GCC/clang assembly output? *)

int sumarr(int *arr){
    register int sum = 0; // register keyword is useful in GCC if you're going to disable optimization
    for (int i=0 ; i<1024 ; i++){
        sum += arr[i];
    }
    return sum;
}

Godbolt-带有-Og的gcc 12使用了您所期望的索引寻址模式;大多数其它优化级别进行指针递增。

# gcc -Og
sumarr:
        movl    $0, %eax
        movl    $0, %edx
        jmp     .L2                   # jump to the loop condition, redundant because 0 <= 1023 is known true; could just fall through into the loop, but -Og maybe intentionally preserves that execution?  It doesn't in general give consistent debugging.
.L3:                                  # do {
        movslq  %eax, %rcx             # sign-extend int to intptr_t since -Og doesn't optimize much
        addl    (%rdi,%rcx,4), %edx    # edx += *(rdi + rcx*4) = array[i]
        addl    $1, %eax
.L2:                                   # loop entry point for first iteration
        cmpl    $1023, %eax
        jle     .L3                   # }while(i<=1023)
        movl    %edx, %eax            # at low optimization levels, GCC didn't sum into the return-value register in the first place.
        ret

对于-O2这样的普通优化级别,我们会得到一个指针增量:

# GCC -O2 -fno-tree-vectorize        (-O2 by itself would vectorize with SIMD)
sumarr:
        leaq    4096(%rdi), %rdx    # endp = ptr + len
        xorl    %eax, %eax          # sum = 0
.L2:                                # do {
        addl    (%rdi), %eax
        addq    $4, %rdi
        cmpq    %rdx, %rdi
        jne     .L2                 # }while(p != endp)
        ret

为局部变量保留的内存量总是16字节的倍数,以保持堆栈对齐为16字节。实际上它应该是0,但它需要是16的倍数。
这是一个过度简化,但在这种情况下,0*16 = 0是16的倍数,它在push rbp之后保持RSP的堆栈对齐。它使用的实际空间在红色区域,在RSP下面128字节。
为局部变量保留的堆栈空间总是8的倍数,奇数或偶数取决于它做了多少次推送,因此RSP的总移动量是16*n + 8
在这种情况下,它不需要像RBX那样保留任何调用保留寄存器,它只推送RBP(因为-fno-omit-frame-pointer-O0的默认值)。
它不支持sub rsp, 16,因为x86-64 SysV ABI包含一个红色区域;RSP下面的128个字节对于信号处理程序或任何异步处理它们的东西都是安全的,因此可以在没有“保留”的情况下使用。如果你想看到GCC为数组保留空间,可以使用-mno-red-zone来阻止GCC这样做。

相关问题