为什么在 arm64 引导代码 __primary_switched 中将 init_task 结构地址保存到 sp_el0?

Why save init_task struct address to sp_el0 in arm64 boot code __primary_switched?

这是来自 linux arm64 (arch/arm64/kernel/head.S) 的汇编代码。(内核源代码 5.4.21)

__primary_switched:
    adrp    x4, init_thread_union   -- line 1
    add sp, x4, #THREAD_SIZE        -- line 2
    adr_l   x5, init_task           -- line 3
    msr sp_el0, x5          // Save thread_info   -- line 4
    adr_l   x8, vectors         // load VBAR_EL1 with virtual  -- line5
    msr vbar_el1, x8            // vector table address  -- line 6
    isb                    -- line7
    
    stp xzr, x30, [sp, #-16]!            -- line8
    mov x29, sp                   -- line9
    
    str_l   x21, __fdt_pointer, x5      // Save FDT pointer   -- line10

我会试着解释一下,如果我错了,请大家指正并纠正我。

我无法准确理解这段代码的作用。特别是第 4 行。这段代码在做什么?

好的,关于 arm64 堆栈管理的速成班:

在 EL1 中,您有两个可以使用 mrs/msr 访问的堆栈指针寄存器:sp_el1sp_el0。您还有一个名为 sp 的寄存器,您可以在大多数其他指令中访问它,例如 addstr 等。然后还有另一个名为 spsel 的系统寄存器,它包括控制 spsp_el1 还是 sp_el0 的别名的单个位。举例说明:

movz x1, 0x1000
movz x2, 0x2000
msr sp_el0, x1
msr sp_el1, x2
msr spsel, 0
add x3, sp, 0x10
msr spsel, 1
add x4, sp, 0x20
// AT this point, x3 == 0x1010 and x4 == 0x2020

另外,当你运行在 EL1 和 eret 到 EL0 时,堆栈指针将始终为 sp_el0。但是这一切的原因是,当你再次对EL1进行异常处理时,你的堆栈指针总是切换到sp_el1。这样做是因为每个通用寄存器都在此时保存用户空间值,并且您需要一种方法来保存它们而不破坏它们中的任何一个(或存储到用户空间内存)。
所以内核通常做的是在 sp_el1 中设置一个异常堆栈,当出现异常时可以将寄存器溢出到该堆栈上。当从 EL1 到 EL1 发生异常(例如 IRQ)时, 应该 通常可以安全地存储在发生异常之前使用的堆栈指针,因此可以运行 内核本身完全在 sp_el1.
然而,大多数操作系统不这样做,而是添加另一个堆栈指针,如果您愿意,可以添加一个“普通内核堆栈”。然后异常流程将如下所示:

  1. EL1 异常,硬件隐式切换到 sp_el1 并禁用中断。
  2. 将所有通用寄存器溢出到异常堆栈。
  3. 用“普通内核堆栈”指针的地址替换sp_el0中的值。
  4. 切换到 spsel, 0 并启用中断。

因为根据您是来自 sp_el0 还是 sp_el1 上的上下文 运行,异常向量会有所不同,这允许您将“预期异常”限制为 sp_el0,如果你在 运行ning on sp_el1 时发生异常,你会认为你在关键部分出错并恐慌。

现在看你展示的代码:它在前四个指令中所做的只是设置异常和“正常”堆栈指针。好像是运行spsel, 1.

另请注意,str_l 不是实际指令,而是 Linux 特定的宏:

/*
 * @src: source register (32 or 64 bit wide)
 * @sym: name of the symbol
 * @tmp: mandatory 64-bit scratch register to calculate the address
 *       while <src> needs to be preserved.
 */
.macro  str_l, src, sym, tmp
adrp    \tmp, \sym
str \src, [\tmp, :lo12:\sym]
.endm

所以它生成的代码是:

adrp x5, __fdt_pointer
str x21, [x5, :lo12:__fdt_pointer]

如您所见,它没有使用 x5 的旧值。

它是 init_thread_union 在内核源代码(v5.10) 中的倒数。

打开 arch/arm64/kernel/vmlinux.lds.S 文件找到 RW_DATA.

. = ALIGN(SEGMENT_ALIGN);
__initdata_end = .;
__init_end = .;

_data = .;
_sdata = .;
RW_DATA(L1_CACHE_BYTES, PAGE_SIZE, THREAD_ALIGN)

跟踪 RW_DATA 显示以下定义。

#define RW_DATA(cacheline, pagealigned, inittask)           \
    . = ALIGN(PAGE_SIZE);                       \
    .data : AT(ADDR(.data) - LOAD_OFFSET) {             \
        INIT_TASK_DATA(inittask)                \
        NOSAVE_DATA                     \
        PAGE_ALIGNED_DATA(pagealigned)              \
        CACHELINE_ALIGNED_DATA(cacheline)           \
        READ_MOSTLY_DATA(cacheline)             \
        DATA_DATA                       \
        CONSTRUCTORS                        \
    }                               \
    BUG_TABLE                           \

在这里,我们再次跟踪 INIT_TASK_DATA。

#define INIT_TASK_DATA(align)                       \
    . = ALIGN(align);                       \
    __start_init_task = .;                      \
    init_thread_union = .;                      \
    init_stack = .;                         \
    KEEP(*(.data..init_task))                   \
    KEEP(*(.data..init_thread_info))                \
    . = __start_init_task + THREAD_SIZE;                \
    __end_init_task = .;

我们可以看到它位于vmlinux.lds.S -> RW_DATA -> INIT_TASK_DATA -> init_thread_union.