为什么编译器使用帧指针和 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),则只需要访问局部变量。在这些架构上,当局部变量的大小固定时,可以使用堆栈指针来访问局部变量,但是这是以增加调试难度为代价的,因此这被视为一种优化。
我试图了解 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),则只需要访问局部变量。在这些架构上,当局部变量的大小固定时,可以使用堆栈指针来访问局部变量,但是这是以增加调试难度为代价的,因此这被视为一种优化。