You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ARM64内核启动代码__primary_switched中为何将init_task地址存入sp_el0?

解析ARM64内核head.S中的__primary_switched代码块

首先贴出你提到的汇编代码:

__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, x8
    vectors是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, sp
    x29是帧指针寄存器(FP),用来标记当前栈帧的底部。把sp的值赋值给x29,完成栈帧的初始化,符合ARM64的ABI规范。

  • Line10: str_l x21, __fdt_pointer, x5
    x21中存储的是设备树(FDT)的物理地址,这行代码通过str_l宏把这个地址保存到__fdt_pointer变量中,让后续内核初始化代码可以解析设备树,获取硬件信息。

整体功能总结

这段代码是ARM64内核启动时,主CPU(primary CPU)完成虚拟地址空间切换后的关键初始化步骤,核心完成了以下工作:

  1. 初始化init进程的内核栈(设置sp_el1);
  2. 初始化当前进程指针(通过sp_el0存储init_task地址,提供current宏的基础);
  3. 配置异常向量表,让内核可以处理后续的异常事件;
  4. 按照ARM64 ABI初始化栈帧,为进入C语言代码执行铺路;
  5. 保存设备树指针,供后续硬件初始化使用。

内容的提问来源于stack exchange,提问作者Chan Kim

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 07:28:27