ARM64内核启动代码__primary_switched中为何将init_task地址存入sp_el0?
首先贴出你提到的汇编代码:
__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
逐行代码解析
先确认你的部分理解是正确的,再补充细节并解答核心疑问:
Line 1:
adrp x4, init_thread_union
你的理解完全正确:adrp指令计算init_thread_union所在页的虚拟地址并加载到x4寄存器。init_thread_union是内核链接脚本(arch/arm64/kernel/vmlinux.lds)定义的连续内存区域,专门给init进程使用。Line 2:
add sp, x4, #THREAD_SIZE
这里设置的是EL1模式下的栈指针sp_el1(此时CPU处于EL1内核态,默认使用sp_el1)。init_thread_union的起始地址是x4,加上THREAD_SIZE后得到栈的顶端高地址(ARM64栈向下增长),后续内核栈会从这里往低地址分配。
纠正一个小细节:ARM64的THREAD_SIZE默认是16KB(CONFIG_THREAD_SIZE_16KB),部分场景下会配置为32KB,不是你提到的4KB哦。Line 3:
adr_l x5, init_task
没错,adr_l是ARM64内核的自定义宏,用来加载init_task的虚拟地址到x5。init_task是内核第一个进程(init进程)的task_struct实例,定义在init/init_task.c中,包含了进程的所有上下文信息。Line 4:
msr sp_el0, x5 // Save thread_info
这是你最困惑的点,详细解释如下:
ARM64架构为每个异常级别配备了独立的栈指针寄存器:sp_el0(EL0用户态栈指针)、sp_el1(EL1内核态栈指针)、sp_el2/sp_el3对应更高特权级。当前CPU处于EL1,操作sp_el0是在设置EL0模式的栈指针,但内核在这里是“借用”这个寄存器做特殊用途。ARM64内核的设计中,
task_struct和thread_info紧密绑定——task_struct的起始地址就是thread_info的起始地址。内核需要一个稳定、随时能快速获取当前进程task_struct的方式,而sp_el1会随栈的使用不断变化,显然不合适。于是内核选择了sp_el0:在EL1模式下,用户态代码无法修改sp_el0,它的值是稳定的,因此把init_task的地址存在这里,作为当前进程的“锚点”。比如内核中的
current宏(获取当前运行进程),在ARM64下就是通过读取sp_el0实现的:#define current get_current() static inline struct task_struct *get_current(void) { struct task_struct *cur; asm ("mrs %0, sp_el0" : "=r" (cur)); return cur; }所以Line4的核心作用是初始化
current指针,让内核知道当前运行的是init_task这个初始进程。你问Line2的
sp和sp_el0是否不同——答案是肯定的!Line2中的sp是sp_el1(当前EL1的栈指针,用于内核栈),而sp_el0是EL0的栈指针,这里被内核复用为当前进程task_struct的存储寄存器,两者是完全不同的寄存器。Line5-6:
adr_l x8, vectors/msr vbar_el1, x8vectors是ARM64内核异常向量表的起始地址,vbar_el1寄存器负责存储EL1模式下的异常向量表基地址。这两行代码告诉CPU:以后发生EL1级别的异常时,跳转到内核提供的向量表去处理。Line7:
isb
这是指令同步屏障(Instruction Synchronization Barrier),用来确保前面修改vbar_el1的指令完全生效后,再执行后续代码。向量表地址的修改需要CPU同步更新状态,避免异常处理出现错误。Line8:
stp xzr, x30, [sp, #-16]!stp是双字存储指令,把零寄存器xzr和链接寄存器x30(存储返回地址)压入栈中,同时栈指针sp_el1向下移动16字节(栈向下增长)。这是按照ARM64的函数调用规范初始化栈帧,为后续跳转到C语言代码执行做准备。Line9:
mov x29, spx29是帧指针寄存器(FP),用来标记当前栈帧的底部。把sp的值赋值给x29,完成栈帧的初始化,符合ARM64的ABI规范。Line10:
str_l x21, __fdt_pointer, x5x21中存储的是设备树(FDT)的物理地址,这行代码通过str_l宏把这个地址保存到__fdt_pointer变量中,让后续内核初始化代码可以解析设备树,获取硬件信息。
整体功能总结
这段代码是ARM64内核启动时,主CPU(primary CPU)完成虚拟地址空间切换后的关键初始化步骤,核心完成了以下工作:
- 初始化init进程的内核栈(设置
sp_el1); - 初始化当前进程指针(通过
sp_el0存储init_task地址,提供current宏的基础); - 配置异常向量表,让内核可以处理后续的异常事件;
- 按照ARM64 ABI初始化栈帧,为进入C语言代码执行铺路;
- 保存设备树指针,供后续硬件初始化使用。
内容的提问来源于stack exchange,提问作者Chan Kim

