为什么编译器使用帧指针和 link 寄存器?

Why is the compiler using the frame pointer and link register?

我试图了解 GNU 如何解释几件事情,所以我的第一个例子非常简单:声明一个整数并打印它。如果没有调用优化,汇编代码为:

    .arch armv8-a
    .file   "ex1.c"
    .text
    .section        .rodata
    .align  3 .LC0:
    .string "%i\n"
    .text
    .align  2
    .global main
    .type   main, %function main: .LFB0:
    .cfi_startproc
    stp     x29, x30, [sp, -32]!
    .cfi_def_cfa_offset 32
    .cfi_offset 29, -32
    .cfi_offset 30, -24
    mov     x29, sp
    str     w0, [sp, 28]
    mov     w0, 328
    str     w0, [sp, 28]
    ldr     w1, [sp, 28]
    adrp    x0, .LC0
    add     x0, x0, :lo12:.LC0
    bl      printf
    nop
    ldp     x29, x30, [sp], 32
    .cfi_restore 30
    .cfi_restore 29
    .cfi_def_cfa_offset 0
    ret
    .cfi_endproc .LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
    .section        .note.GNU-stack,"",@progbits

如果编译优化(-O3),汇编代码更简洁:

    .arch armv8-a
    .file   "ex1.c"
    .text
    .section        .rodata.str1.8,"aMS",@progbits,1
    .align  3 .LC0:
    .string "%i\n"
    .section        .text.startup,"ax",@progbits
    .align  2
    .p2align 3,,7
    .global main
    .type   main, %function main: .LFB23:
    .cfi_startproc
    adrp    x1, .LC0
    mov     w2, 328
    add     x1, x1, :lo12:.LC0
    mov     w0, 1
    b       __printf_chk
    .cfi_endproc .LFE23:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
    .section        .note.GNU-stack,"",@progbits

除了 p2align 3,7 之外,大多数东西都相对简单,我在阅读源软件上的描述后仍在弄清楚它。但是,我的主要问题是别的。为什么未优化的版本使用帧指针和 link 注册以及 CFA?它试图完成什么?有人可能想知道我为什么关心,选择优化版本。原因是 Fortran 代码的优化版本恢复到与使用帧指针和 link 寄存器的未优化 C 版本相似。
Fortran 代码很简单:

  program integer_printing

  integer (kind=4) a
  a=328
  write (*,*) a
  end

优化后的汇编代码为

        .arch armv8-a
        .file   "exa1F.f90"
        .text
        .section        .rodata.str1.8,"aMS",@progbits,1
        .align  3
.LC0:
        .string "exa1F.f90"
        .text
        .align  2
        .p2align 3,,7
        .type   MAIN__, %function
MAIN__:
.LFB0:
        .cfi_startproc
        sub     sp, sp, #576
        .cfi_def_cfa_offset 576
        adrp    x0, .LC1
        adrp    x1, .LC0
        add     x1, x1, :lo12:.LC0
        mov     w3, 328
        mov     w2, 5
        stp     x29, x30, [sp]
        .cfi_offset 29, -576
        .cfi_offset 30, -568
        mov     x29, sp
        ldr     d0, [x0, #:lo12:.LC1]
        str     x19, [sp, 16]
        .cfi_offset 19, -560
        add     x19, sp, 48
        mov     x0, x19
        str     w3, [sp, 44]
        str     d0, [sp, 48]
        str     x1, [sp, 56]
        str     w2, [sp, 64]
        bl      _gfortran_st_write
        add     x1, sp, 44
        mov     w2, 4
        mov     x0, x19
        bl      _gfortran_transfer_integer_write
        mov     x0, x19
        bl      _gfortran_st_write_done
        ldp     x29, x30, [sp]
        ldr     x19, [sp, 16]
        add     sp, sp, 576
        .cfi_restore 29
        .cfi_restore 30
        .cfi_restore 19
        .cfi_def_cfa_offset 0
        ret
        .cfi_endproc
.LFE0:
        .size   MAIN__, .-MAIN__
        .section        .text.startup,"ax",@progbits
        .align  2
        .p2align 3,,7
        .global main
        .type   main, %function
main:
.LFB1:
        .cfi_startproc
        stp     x29, x30, [sp, -16]!
        .cfi_def_cfa_offset 16
        .cfi_offset 29, -16
        .cfi_offset 30, -8
        mov     x29, sp
        bl      _gfortran_set_args
        adrp    x1, .LANCHOR0
        add     x1, x1, :lo12:.LANCHOR0
        mov     w0, 7
        bl      _gfortran_set_options
        bl      MAIN__
        mov     w0, 0
        ldp     x29, x30, [sp], 16
        .cfi_restore 30
        .cfi_restore 29
        .cfi_def_cfa_offset 0
        ret
        .cfi_endproc
.LFE1:
        .size   main, .-main
        .section        .rodata.cst8,"aM",@progbits,8
        .align  3
.LC1:
        .word   128
        .word   6
        .section        .rodata
        .align  3
        .set    .LANCHOR0,. + 0
        .type   options.1.2778, %object
        .size   options.1.2778, 28
options.1.2778:
        .word   2116
        .word   4095
        .word   0
        .word   1
        .word   1
        .word   0
        .word   31
        .ident  "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
        .section        .note.GNU-stack,"",@progbits

出于某种原因,在使用 GCC 为 ARM64 目标(包括 GNU Fortran)编译时,-O3 没有打开 -fomit-frame-pointer 选项。您需要 enable this option explicitly 让编译器优化非叶函数中帧指针的使用:

MAIN__:
        adrp    x0, .LC1
        sub     sp, sp, #560
        adrp    x1, .LC0
        add     x1, x1, :lo12:.LC0
        ldr     d0, [x0, #:lo12:.LC1]
        mov     w3, 328
        mov     w2, 5
        add     x0, sp, 32
        str     x30, [sp]
        str     w3, [sp, 28]
        str     d0, [sp, 32]
        str     x1, [sp, 40]
        str     w2, [sp, 48]
        bl      _gfortran_st_write
        add     x1, sp, 28
        mov     w2, 4
        add     x0, sp, 32
        bl      _gfortran_transfer_integer_write
        add     x0, sp, 32
        bl      _gfortran_st_write_done
        ldr     x30, [sp]
        add     sp, sp, 560
        ret

当编译器将对 printf (bl printf) 的尾部调用更改为跳转 (b __printf_chk ).这就是为什么在不使用 -fomit-frame-pointer.

的情况下删除帧指针的原因

请注意,link 寄存器永远不会被优化掉,至少在任何可以 return 到它的调用者的函数中都不会,因为它需要保留这个寄存器中的值,因为它包含到 return 的地址。在优化的 C 示例中,编译器不需要保存或恢复 link 寄存器 (X30)。它只是保持不变,因此 __printf_chk return 直接发送给示例 C 函数的调用者。在您的其他示例中,存储在 link 寄存器中的值被调用这些函数 make 的函数破坏(特别是通过 BL 指令),因此需要保存和恢复。

最后,帧指针与C指针无关。编译器使用它来访问函数的局部变量,还形成一个 linked 堆栈帧列表,调试器可以使用它来创建回溯和检查调用函数的局部变量。然而,在大多数架构上,如果函数进行可变大小的堆栈分配(例如 variable length arrays),则只需要访问局部变量。在这些架构上,当局部变量的大小固定时,可以使用堆栈指针来访问局部变量,但是这是以增加调试难度为代价的,因此这被视为一种优化。