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

Cha*_*Kim 3 boot assembly linux-kernel arm64

这是来自 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
Run Code Online (Sandbox Code Playgroud)

我会尽力解释它,如果我错了,请有人给我启发并纠正我。

  • 第 1 行:x4 =(init_thread_union 的页地址)。我发现 init_thread_union 是内核链接描述文件中的一个变量。(arch/arm64/kernel/vmlinux.lds)。这个 vmlinux.lds 是在内核构建期间从 vmlinux.lds.S 生成的。
  • 第 2 行:sp = (x4 + #THREAD_SIZE)。看起来像是为此线程设置堆栈指针。(看起来这个线程正在使用 init_thread_union 内存区域)(使用 init_thread_union 位置中的 4K 字节作为该线程的堆栈)
  • 第3行:x5 =(init_task结构的地址),我发现init_task是init任务的task_struct。(在init/init_task.c中)。所以这个结构体包含线程信息。
  • 第 4 行:sp_el0 = x5。为什么要使用thread_info设置异常级别0的堆栈指针?这个 sp_el0 与第 2 行中的 sp 不同吗?(我猜我们现在在 el 1 中,所以第 2 行中的 sp 意味着 sp_el1)。x5 随后在第 10 行中使用。

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

Sig*_*uza 8

好吧,arm64 上的堆栈管理速成课程:

在 EL1 中,您有两个堆栈指针寄存器,您可以使用mrs/ msr:sp_el1和 来访问它们sp_el0。您还有一个名为 的寄存器sp,您可以在大多数其他指令(如addstr等)中访问该寄存器。然后还有另一个名为 的系统寄存器,它由一个控制是否为或的别名的spsel位组成。为了显示:spsp_el1sp_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
Run Code Online (Sandbox Code Playgroud)

此外,当您在 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,并且如果您在 上运行时发生异常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
Run Code Online (Sandbox Code Playgroud)

所以它生成的代码是:

adrp x5, __fdt_pointer
str x21, [x5, :lo12:__fdt_pointer]
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,其中不使用 的旧值x5