为什么在 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
我会试着解释一下,如果我错了,请大家指正并纠正我。
- 第 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 是初始化任务的 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 行。这段代码在做什么?
好的,关于 arm64 堆栈管理的速成班:
在 EL1 中,您有两个可以使用 mrs
/msr
访问的堆栈指针寄存器:sp_el1
和 sp_el0
。您还有一个名为 sp
的寄存器,您可以在大多数其他指令中访问它,例如 add
、str
等。然后还有另一个名为 spsel
的系统寄存器,它包括控制 sp
是 sp_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
.
然而,大多数操作系统不这样做,而是添加另一个堆栈指针,如果您愿意,可以添加一个“普通内核堆栈”。然后异常流程将如下所示:
- EL1 异常,硬件隐式切换到
sp_el1
并禁用中断。
- 将所有通用寄存器溢出到异常堆栈。
- 用“普通内核堆栈”指针的地址替换
sp_el0
中的值。
- 切换到
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.
这是来自 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
我会试着解释一下,如果我错了,请大家指正并纠正我。
- 第 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 是初始化任务的 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 行。这段代码在做什么?
好的,关于 arm64 堆栈管理的速成班:
在 EL1 中,您有两个可以使用 mrs
/msr
访问的堆栈指针寄存器:sp_el1
和 sp_el0
。您还有一个名为 sp
的寄存器,您可以在大多数其他指令中访问它,例如 add
、str
等。然后还有另一个名为 spsel
的系统寄存器,它包括控制 sp
是 sp_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
.
然而,大多数操作系统不这样做,而是添加另一个堆栈指针,如果您愿意,可以添加一个“普通内核堆栈”。然后异常流程将如下所示:
- EL1 异常,硬件隐式切换到
sp_el1
并禁用中断。 - 将所有通用寄存器溢出到异常堆栈。
- 用“普通内核堆栈”指针的地址替换
sp_el0
中的值。 - 切换到
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.