为什么在许多 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)。
这来自 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)。