0

这是来自 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)

据我了解,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

我不明白这段代码如何使它返回到lr. 我们不应该做类似的事情mv pc, lr吗?

4

1 回答 1

5

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

这种优化的工作方式是,调用者 A 调用函数 B,该函数调用另一个函数 C。如果 B 在调用 C 后要直接返回 A,那么 B 可以跳转到 C!再聪明不过,C 返回到它的调用者,它似乎是 A。通过这样做,B 不需要堆栈帧,也不必保存链接寄存器 - 它只需将其返回地址传递给 C。


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

  • 如果C返回B无事可做,B可以设置C直接返回A。
  • 从逻辑的角度来看(例如在 C 或伪代码中),这意味着:
    • B 和 C 都是 void 函数,或者,
    • 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应用优化模式中的函数(C 也可以,例如如果 C 调用 D)。

于 2020-11-14T09:13:22.257 回答