Assembly 为什么编译器使用帧指针和链接寄存器?
我试图理解GNU是如何解释一些事情的,所以我的第一个示例非常简单:声明一个整数并打印它。如果未调用优化,则汇编代码将显示: 如果编译已优化(-O3),则汇编代码将更加精简: 除了p2align 3,7之外,大多数内容都相对简单,在阅读了sourceware上的描述之后,我仍然在弄清楚它。然而,我的主要问题是别的。为什么非优化版本使用帧指针和链接寄存器以及CFA?它试图实现什么?人们可能会想,为什么我会在意,选择优化版。原因是Fortran代码的优化版本通过使用帧指针和链接寄存器恢复为类似于未优化的C版本。Assembly 为什么编译器使用帧指针和链接寄存器?,assembly,fortran,gfortran,gnu-assembler,Assembly,Fortran,Gfortran,Gnu Assembler,我试图理解GNU是如何解释一些事情的,所以我的第一个示例非常简单:声明一个整数并打印它。如果未调用优化,则汇编代码将显示: 如果编译已优化(-O3),则汇编代码将更加精简: 除了p2align 3,7之外,大多数内容都相对简单,在阅读了sourceware上的描述之后,我仍然在弄清楚它。然而,我的主要问题是别的。为什么非优化版本使用帧指针和链接寄存器以及CFA?它试图实现什么?人们可能会想,为什么我会在意,选择优化版。原因是Fortran代码的优化版本通过使用帧指针和链接寄存器恢复为类似于未优化
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
无论出于何种原因,
-O3
在使用GCC为ARM64目标(包括GNU Fortran)编译时,都不会打开-fomit frame pointer
选项。您需要为编译器优化在非叶函数中使用帧指针:
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\u printf\u chk
)时,您的C示例代码将优化为一个叶函数,该叶函数不调用其他函数。这就是为什么在不使用-fomit frame pointer
的情况下消除帧指针的原因
请注意,链接寄存器永远不会被优化掉,至少在任何可以返回其调用者的函数中都不会,因为它需要保留该寄存器中的值,因为它包含要返回的地址。在优化的C示例中,编译器不需要保存或恢复链接寄存器(X30)。它只是让它保持不变,所以\uu printf\u chk
直接返回给示例C函数的调用者。在您的其他示例中,链接寄存器中存储的值会被这些函数进行的函数调用(特别是BL指令)破坏,因此需要保存和恢复
最后,帧指针与C指针无关。编译器使用它访问函数的局部变量,还形成堆栈帧的链接列表,调试器可以使用它创建回溯并检查调用函数的局部变量。然而,在大多数架构上,如果函数进行可变大小的堆栈分配(例如for),则只需要访问局部变量。在这些体系结构上,当局部变量的大小固定时,可以使用堆栈指针来访问局部变量,但是,这是以增加调试难度为代价的,因此这被视为一种优化。无论出于何种原因,
-O3
在使用GCC为ARM64目标(包括GNU Fortran)编译时,都不会启用-fomit frame pointer
选项。您需要为编译器优化在非叶函数中使用帧指针:
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\u printf\u chk
)时,您的C示例代码将优化为一个叶函数,该叶函数不调用其他函数。这就是为什么在不使用-fomit frame pointer
的情况下消除帧指针的原因
请注意,链接寄存器永远不会被优化掉,至少在任何可以返回其调用者的函数中都不会,因为它需要保留该寄存器中的值,因为它包含要返回的地址。在优化的C示例中,编译器不需要保存或恢复链接寄存器(X30)。它只是让它保持不变,所以\uu printf\u chk
直接返回给示例C函数的调用者。在您的其他示例中,链接寄存器中存储的值会被这些函数进行的函数调用(特别是BL指令)破坏,因此需要保存和恢复
最后,帧指针与C指针无关。编译器使用它访问函数的局部变量,还形成堆栈帧的链接列表,调试器可以使用它创建回溯并检查调用函数的局部变量。然而,在大多数架构上,如果函数进行可变大小的堆栈分配(例如for),则只需要访问局部变量。在这些体系结构上,当局部变量的大小固定时,可以使用堆栈指针来访问局部变量,但这是以增加调试难度为代价的,因此这被视为一种优化。在未优化的代码中使用帧指针来帮助调试。优化版本还使用了跳转到
printf
(尾部调用消除)。如果有动态堆栈分配,也可以使用帧指针。好的,这回答了我的大部分问题。我仍然不完全理解为什么优化的Fortran版本使用帧指针(可能是因为“经典”Fortran不使用指针),因为代码肯定没有使用可分配变量。谢谢。你问错问题了。与其询问无关的C代码,不如发布您的Fortran代码及其生成的汇编代码,并询问为什么它在启用优化的情况下使用帧指针。我编辑了这篇文章以包含Fortran代码和汇编代码。来自未优化C编译的汇编代码仍然令人感兴趣,因为GNU编译器使用帧指针作为优化的Fortran编译(这是我试图弄清楚的一点)。未优化代码中使用帧指针来帮助调试。优化版本还使用了跳转到printf
(尾部调用消除)。如果有动态堆栈分配,也可以使用帧指针。好的,这回答了我的大部分问题。我仍然不完全理解为什么优化后的Fortran版本使用
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