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

why is there a b (branch) instruction just before the end of a procedure in many arm64 procedures?

这来自 linux 来源 arch/arm64/kernel/head.S 显示内核启动。该代码首先调用 preserve_boot_args,然后使用 bl(分支和 link)调用 el2_setup。我也展示了程序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)

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

#define SYM_END(name, sym_type)                         \
        .type name sym_type ASM_NL                      \
        .size name, .-name
#endif

我不明白这段代码如何使它 return 到 lr 中的地址。我们不应该做类似 mv pc, lr 的事情吗?

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

此优化的工作方式,调用者 A 调用一个函数 B,该函数又调用另一个函数 C。如果 B 在调用 C 后要 return 直接转到 A,则 B 可以跳转改为C! none 更聪明,C returns 对它的调用者来说,它似乎是 A。通过这样做,B 不需要堆栈帧,也不必保存 link 注册 — 它只是将其 return 地址传递给 C.


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

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

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


我们无法用大多数高级语言表达尾调用优化,因为大多数语言只有“调用子程序”而没有“跳转到子程序”功能。因此,充其量,我们可以按照上述编写对 return 不起作用的代码,并让 language/compiler 执行优化(如果它知道优化)。


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

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