为什么在许多 arm64 程序中的程序结束之前有 ab(分支)指令?

Cha*_*Kim 0 assembly arm abi eabi arm64

这是来自 linux 源代码 arch/arm64/kernel/head.S 显示内核启动。代码首先调用preserve_boot_args和下一个调用el2_setup使用bl(分支和链接)。我也展示了程序preserve_boot_args

SYM_CODE_START(primary_entry)
        bl      preserve_boot_args
        bl      el2_setup                       // Drop to EL1, w0=cpu_boot_mode
        adrp    x23, __PHYS_OFFSET
        and     x23, x23, MIN_KIMG_ALIGN - 1    // KASLR offset, defaults to 0
        bl      set_cpu_boot_mode_flag
        bl      __create_page_tables
        /*
         * The following calls CPU setup code, see arch/arm64/mm/proc.S for
         * details.
         * On return, the CPU will be ready for the MMU to be turned on and
         * the TCR will have been set.
         */
        bl      __cpu_setup                     // initialise processor
        b       __primary_switch
SYM_CODE_END(primary_entry)

SYM_CODE_START_LOCAL(preserve_boot_args)
        mov     x21, x0                         // x21=FDT

        adr_l   x0, boot_args                   // record the contents of
        stp     x21, x1, [x0]                   // x0 .. x3 at kernel entry
        stp     x2, x3, [x0, #16]

        dmb     sy                              // needed before dc ivac with
                                                // MMU off

        mov     x1, #0x20                       // 4 x 8 bytes
        b       __inval_dcache_area             // tail call
SYM_CODE_END(preserve_boot_args)
Run Code Online (Sandbox Code Playgroud)

据我了解,bl用于调用过程(在过程之后,返回到 lr - 链接寄存器中保存的地址,x30)并且b只是去不返回的标记地址。但是在preserve_boot_args上面的程序中,就在最后,有一条b __inval_dcache_area指令直接去__inval_dcache_area而不返回。那么它如何返回到原始代码(在哪里bl el2_setup)?程序如何结束?SYM_CODE_END 的定义是这样的:

#define SYM_END(name, sym_type)                         \
        .type name sym_type ASM_NL                      \
        .size name, .-name
#endif
Run Code Online (Sandbox Code Playgroud)

我无法理解这段代码是如何让它返回到lr. 我们不应该做类似的事情mv pc, lr吗?

Eri*_*idt 5

这看起来像是调用优化——有时也称为尾调用优化,这有助于减少递归的堆栈深度——但在一般情况下也很有用。

这种优化的工作方式是,调用者 A 调用一个函数 B,后者调用另一个函数 C。如果 B 在调用 C 后要直接返回 A,那么 B 可以改为跳转到 C!由于没有更聪明,C 返回到它的调用者,它看起来是 A。 通过这样做,B 不需要堆栈帧并且不必保存链接寄存器——它只是将它的返回地址传递给 C。


这种优化跳过了 C 到 B 的正常返回,使 C 直接返回到 A。 这种转换仅在某些情况下启用(即正确):

  • 如果没有工作对于B在C'S回做,B可以设置C到直接返回A.
  • 从逻辑角度(例如在 C 或伪代码中),这意味着:
    • B 和 C 都是空函数,或者,
    • B 忽略 C 的返回值,或者,
    • B 将 C 的返回值返回给 A,未修改
  • B 也不能在 C 返回后清理堆栈帧,因为 C 直接返回到 A;如果 B 有一个堆栈帧,则必须在调用 C 之前将其释放。(另请参阅下面的@PeterCordes 评论。)

从硬件的角度来看,当使用优化时(在 B 中编码,然后调用 B),就好像 B 和 C 合并了:如果你愿意,函数 A 调用“BC”。动态地,有一个bl(A->BC) 和一个ret(BC->A) — 很好地平衡,这有利于硬件分支预测器的调用堆栈处理。


我们无法在大多数高级语言中表达尾调用优化,因为大多数语言只有“调用子程序”而没有“跳转到子程序”功能。因此,充其量,我们可以编写如上所述在返回时不起作用的代码,并让语言/编译器执行优化,如果它知道优化。


在 A 调用 B 调用 C,B & C 是函数,但 A 可能是也可能不是函数(它可能只是一些汇编代码——虽然它是 B 的调用者,但 A 本身不需要被调用或调用一个函数。虽然调用链可能很深,但调用链最顶端的第一个代码不是函数(例如它是_start或有时是main)并且没有返回的地方(所以不ret用于退出;它不会'没有调用者提供的返回地址参数。(如果代码有返回位置,即要使用的返回地址,那么根据定义,它不是调用链的顶部(名义上是一个函数)。

这个初始代码可以在模式中扮演 A 而不是 B 或 C 的角色。当 A 不是函数时,A 对 B 的调用将排除尾调用,因为 B 没有返回到 A 的调用者。这就是为什么模式必须是A调用B调用C,B&C必须是函数,我们考虑将优化应用到B。如果A是函数,它必须有一个调用者,这样才能起到中间的作用模式中的函数(C 也可以,例如,如果 C 调用 D)。