ARM64启动汇编和内存初始化(中) --- (二)
接上文。
2.5 初始化cpu状态(__cpu_setup)
虽然在异常初始化流程中已经设置了sctlr_el1等系统控制寄存器,但在打开mmu前还需要其它一些准备工作。
...... # arch/arm64/mm/proc.S /* * __cpu_setup * * Initialise the processor for turning the MMU on. * * Output: * Return in x0 the value of the SCTLR_EL1 register. */ .pushsection ".idmap.text", "awx" '放在.idmap.text段中' SYM_FUNC_START(__cpu_setup) tlbi vmalle1 // Invalidate local TLB dsb nsh mov x1, #3 << 20 msr cpacr_el1, x1 // Enable FP/ASIMD ---(2.5.1) mov x1, #1 << 12 // Reset mdscr_el1 and disable ---(2.5.2) msr mdscr_el1, x1 // access to the DCC from EL0 isb // Unmask debug exceptions now, enable_dbg // since this is per-cpu ---(2.5.3) reset_pmuserenr_el0 x1 // Disable PMU access from EL0 关闭EL0访问PMU reset_amuserenr_el0 x1 // Disable AMU access from EL0 关闭EL0访问AMU /* * Default values for VMSA control registers. These will be adjusted * below depending on detected CPU features. */ //name .req register name: 为寄存器定义一个别名 mair .req x17 //定义X17寄存器的别名 mair tcr .req x16 //定义X16寄存器的别名 tcr mov_q mair, MAIR_EL1_SET // //---(2.5.4) mov_q tcr, TCR_TxSZ(VA_BITS) | TCR_CACHE_FLAGS | TCR_SMP_FLAGS | \ //---(2.5.5) TCR_TG_FLAGS | TCR_KASLR_FLAGS | TCR_ASID16 | \ TCR_TBI0 | TCR_A1 | TCR_KASAN_SW_FLAGS #ifdef CONFIG_ARM64_MTE //---(2.5.6) /* * Update MAIR_EL1, GCR_EL1 and TFSR*_EL1 if MTE is supported * (ID_AA64PFR1_EL1[11:8] > 1). */ mrs x10, ID_AA64PFR1_EL1 ubfx x10, x10, #ID_AA64PFR1_MTE_SHIFT, #4 cmp x10, #ID_AA64PFR1_MTE b.lt 1f /* Normal Tagged memory type at the corresponding MAIR index */ mov x10, #MAIR_ATTR_NORMAL_TAGGED bfi mair, x10, #(8 * MT_NORMAL_TAGGED), #8 mov x10, #KERNEL_GCR_EL1 msr_s SYS_GCR_EL1, x10 /* * If GCR_EL1.RRND=1 is implemented the same way as RRND=0, then * RGSR_EL1.SEED must be non-zero for IRG to produce * pseudorandom numbers. As RGSR_EL1 is UNKNOWN out of reset, we * must initialize it. */ mrs x10, CNTVCT_EL0 ands x10, x10, #SYS_RGSR_EL1_SEED_MASK csinc x10, x10, xzr, ne lsl x10, x10, #SYS_RGSR_EL1_SEED_SHIFT msr_s SYS_RGSR_EL1, x10 /* clear any pending tag check faults in TFSR*_EL1 */ msr_s SYS_TFSR_EL1, xzr msr_s SYS_TFSRE0_EL1, xzr /* set the TCR_EL1 bits */ mov_q x10, TCR_MTE_FLAGS orr tcr, tcr, x10 1: #endif tcr_clear_errata_bits tcr, x9, x5 // ---(2.5.7) #ifdef CONFIG_ARM64_VA_BITS_52 //假设我使用的48bit ldr_l x9, vabits_actual sub x9, xzr, x9 add x9, x9, #64 tcr_set_t1sz tcr, x9 #else ldr_l x9, idmap_t0sz // ---(2.5.8-1)idmap_t0sz = TCR_T0SZ(VA_BITS_MIN) = 16 #endif tcr_set_t0sz tcr, x9 // ---(2.5.8-2)tcr寄存器的T0SZ域 /* * Set the IPS bits in TCR_EL1. */ tcr_compute_pa_size tcr, #TCR_IPS_SHIFT, x5, x6 // ---(2.5.9) #ifdef CONFIG_ARM64_HW_AFDBM // ---(2.5.10) /* * Enable hardware update of the Access Flags bit. * Hardware dirty bit management is enabled later, * via capabilities. */ mrs x9, ID_AA64MMFR1_EL1 and x9, x9, #0xf cbz x9, 1f orr tcr, tcr, #TCR_HA // hardware Access flag update 1: #endif /* CONFIG_ARM64_HW_AFDBM */ msr mair_el1, mair //Memory Attribute Indirection Register (EL1)真正的设置动作 msr tcr_el1, tcr //Translation Control Register (EL1)真正的设置动作 /* * Prepare SCTLR */ mov_q x0, INIT_SCTLR_EL1_MMU_ON // ---(2.5.11) ret // return to head.S .unreq mair //.unreq用来取消一个寄存器的别名 .unreq tcr SYM_FUNC_END(__cpu_setup)
2.5.1 设置EL0和EL1异常等级可以访问浮点运算单元

2.5.2 调试监控系统寄存器

2.5.3 设置PSTATE寄存器的调试掩码域
宏enable_dbg定义在/arch/arm64/include/asm/assembler.h中
.macro enable_dbg msr daifclr, #8 .endm

2.5.4 内存属性值赋值给mair(x17)
armv8内存可分为device memory和normal memory,它们又可以具有不同的属性,如:
- device memory可配置不同的nGnRnE属性,以确定其访问内存时的行为;
- normal memory可以有不同的cache策略,如cache写回、cache写通或者non cache等。寄存器MAIR_EL1用于设置内存属性表,它按八位一组分成了八组属性,其定义如下图:
宏MAIR_EL1_SET定义在arch/arm64/mm/proc.S文件中,
/* * Default MAIR_EL1. MT_NORMAL_TAGGED is initially mapped as Normal memory and * changed during __cpu_setup to Normal Tagged if the system supports MTE. */ #define MAIR_EL1_SET \ (MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRnE, MT_DEVICE_nGnRnE) | \ MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRE, MT_DEVICE_nGnRE) | \ MAIR_ATTRIDX(MAIR_ATTR_NORMAL_NC, MT_NORMAL_NC) | \ MAIR_ATTRIDX(MAIR_ATTR_NORMAL, MT_NORMAL) | \ MAIR_ATTRIDX(MAIR_ATTR_NORMAL, MT_NORMAL_TAGGED))
2.5.5 管理页表映射的值赋值给tcr --- Translation Control Register寄存器(x16)

2.5.6 MTE
MTE 是ARM新架构(ARM V8.5 引入)的一个特性,它通过给分配的内存打标记(tag),追踪最常见的非法内存操作。如果密钥的值和锁的值一样,表示访问成功,否则会报告一个错误。ARM MTE简介
2.5.7 清除触发此CPU上错误的TCR位
宏tcr_clear_errata_bits定义在/arch/arm64/include/asm/assembler.h中
/* * tcr_clear_errata_bits - Clear TCR bits that trigger an errata on this CPU. */ .macro tcr_clear_errata_bits, tcr, tmp1, tmp2 #ifdef CONFIG_FUJITSU_ERRATUM_010001 mrs \tmp1, midr_el1 mov_q \tmp2, MIDR_FUJITSU_ERRATUM_010001_MASK and \tmp1, \tmp1, \tmp2 mov_q \tmp2, MIDR_FUJITSU_ERRATUM_010001 cmp \tmp1, \tmp2 b.ne 10f mov_q \tmp2, TCR_CLEAR_FUJITSU_ERRATUM_010001 bic \tcr, \tcr, \tmp2 10: #endif /* CONFIG_FUJITSU_ERRATUM_010001 */ .endm
2.5.8 设置TTBR0_EL1寻址的内存区域的大小为2^(64-T0SZ)字节。

2.5.9 设置TCR_EL1中的IPS位为ID_AA64MMFR0_EL1.PARange支持的最高值
ID_AA64MMFR0_EL1 - Memory model feature register 0
/* * tcr_compute_pa_size - set TCR.(I)PS to the highest supported * ID_AA64MMFR0_EL1.PARange value * * tcr: register with the TCR_ELx value to be updated * pos: IPS or PS bitfield position * tmp{0,1}: temporary registers */ .macro tcr_compute_pa_size, tcr, pos, tmp0, tmp1 //pos =32 mrs \tmp0, ID_AA64MMFR0_EL1 // Narrow PARange to fit the PS field in TCR_ELx //UBFX Xd, Xn, #lsb, #width //ubfx指令是无符号位域提取指令:UBFX指令的意思是从Wn寄存器的第lsb位开始,提取width位到Wd寄存器,剩余高位用0填充。 ubfx \tmp0, \tmp0, #ID_AA64MMFR0_PARANGE_SHIFT, #3 //ID_AA64MMFR0_PARANGE_SHIFT = 0 mov \tmp1, #ID_AA64MMFR0_PARANGE_MAX //根据配置可能是48/52 cmp \tmp0, \tmp1 //CSEL Xd, Xn, Xm, cond :条件选择,返回第一个或第二个输入 csel \tmp0, \tmp1, \tmp0, hi //hi:无符号数大于 bfi \tcr, \tmp0, \pos, #3 .endm
2.5.10 用硬件实现更新访问脏页面的标记位
ARMV8.1支持的硬件特性。如果支持了这个硬件特性,访问一个物理页面时,硬件会自动设置PTE的AF域,否则需要软件(缺页中断)方式来模拟。
2.5.11 系统控制寄存器(SCTLR)域值
并未设置SCTLR在,只是作为一个参数,传给下一个函数。
2.6 C运行时环境初始化(__primary_switch )
_primary_switch主要用于设置c运行时环境,如使能MMU,设置异常向量表,栈,BSS段等,最后跳转到C语言函数start_kernel 。该函数具体实现如下,我们暂且跳过与主流程关联较小的kaslr和内核重定向相关代码。
SYM_FUNC_START_LOCAL(__primary_switch) // __primary_switch在恒等映射段中(物理地址==虚拟地址) #ifdef CONFIG_RANDOMIZE_BASE // ---(2.6.1) mov x19, x0 // preserve new SCTLR_EL1 value mrs x20, sctlr_el1 // preserve old SCTLR_EL1 value #endif adrp x1, init_pg_dir // kernel image的映射使用的页表起始地址【重点关注,下面TTBR1会用这个值】 bl __enable_mmu // ---(2.6.2) #ifdef CONFIG_RELOCATABLE #ifdef CONFIG_RELR mov x24, #0 // no RELR displacement yet #endif bl __relocate_kernel #ifdef CONFIG_RANDOMIZE_BASE ldr x8, =__primary_switched adrp x0, __PHYS_OFFSET blr x8 /* * If we return here, we have a KASLR displacement in x23 which we need * to take into account by discarding the current kernel mapping and * creating a new one. */ pre_disable_mmu_workaround msr sctlr_el1, x20 // disable the MMU isb bl __create_page_tables // recreate kernel mapping tlbi vmalle1 // Remove any stale TLB entries dsb nsh isb set_sctlr_el1 x19 // re-enable the MMU bl __relocate_kernel #endif #endif ldr x8, =__primary_switched //x8 = __primary_switched编译后的链接地址,即虚拟地址【重点关注】 adrp x0, __PHYS_OFFSET //_text的值(kernel image存放的物理地址) br x8 //无条件跳转到芯片支持的所有地址范围,dst→x8 ---(2.6.3) SYM_FUNC_END(__primary_switch)
2.6.1 内核地址空间布局随机化
主要是为了防止黑客的攻击,在早期阶段内核镜像映射到虚拟地址空间的地址是固定的,黑客利用这个特性很容易进行攻击。
2.6.2 打开MMU
/* * Enable the MMU. * * x0 = SCTLR_EL1 value for turning on the MMU. //2.5.11 * x1 = TTBR1_EL1 value //2.6 * * Returns to the caller via x30/lr. This requires the caller to be covered * by the .idmap.text section. * * Checks if the selected granule size is supported by the CPU. * If it isn't, park the CPU */ SYM_FUNC_START(__enable_mmu) mrs x2, ID_AA64MMFR0_EL1 // ubfx x2, x2, #ID_AA64MMFR0_TGRAN_SHIFT, 4 //假设我们定义了CONFIG_ARM64_4K_PAGES:这里就是ID_AA64MMFR0_EL1.TGran4域(bits [31:28])的值赋值给x2寄存器 cmp x2, #ID_AA64MMFR0_TGRAN_SUPPORTED_MIN //ID_AA64MMFR0_TGRAN_SUPPORTED_MIN=0 b.lt __no_granule_support //非法粒度 cmp x2, #ID_AA64MMFR0_TGRAN_SUPPORTED_MAX //ID_AA64MMFR0_TGRAN_SUPPORTED_MIN=7 b.gt __no_granule_support //非法粒度 update_early_cpu_boot_status 0, x2, x3 //把0写入到全局变量__early_cpu_boot_status中 adrp x2, idmap_pg_dir //加载idmap_pg_dir的物理地址到x2寄存器,idmap_pg_dir是恒等映射的一级页表起始地址【重点关注,下面TTBR0会用这个值】 phys_to_ttbr x1, x1 //对于48bit不进行操作 phys_to_ttbr x2, x2 //对于48bit不进行操作 msr ttbr0_el1, x2 // load TTBR0:Translation Table Base Register 0 (EL1) offset_ttbr1 x1, x3 msr ttbr1_el1, x1 // load TTBR1:Translation Table Base Register 0 (EL1) isb set_sctlr_el1 x0 //打开mmu ret SYM_FUNC_END(__enable_mmu)
2.6.3 __primary_switched
此时MMU已经打开,我们运行在虚拟地址空间内。
/* * The following fragment of code is executed with the MMU enabled. * * x0 = __PHYS_OFFSET */ SYM_FUNC_START_LOCAL(__primary_switched) adr_l x4, init_task init_cpu_task x4, x5, x6 // ---(1) adr_l x8, vectors // load VBAR_EL1 with virtual msr vbar_el1, x8 // vector table address ---(2) isb stp x29, x30, [sp, #-16]! // ---(3) mov x29, sp str_l x21, __fdt_pointer, x5 // Save FDT pointer ---(4) ldr_l x4, kimage_vaddr // Save the offset between sub x4, x4, x0 // the kernel virtual and str_l x4, kimage_voffset, x5 // physical mappings ---(5) // Clear BSS ---(6) adr_l x0, __bss_start // 起始地址 mov x1, xzr // 要写入的值,xzr是一个特殊的寄存器,值为64位的0 adr_l x2, __bss_stop // 结束地址 sub x2, x2, x0 // size = __bss_stop - __bss_start bl __pi_memset // memset(x0, x1, x2) dsb ishst // Make zero page visible to PTW #if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS) bl kasan_early_init #endif mov x0, x21 // pass FDT address in x0 bl early_fdt_map // Try mapping the FDT early ---(7) bl init_feature_override // Parse cpu feature overrides 根据BootLoader传入的参数,对一些参数的改写 #ifdef CONFIG_RANDOMIZE_BASE tst x23, ~(MIN_KIMG_ALIGN - 1) // already running randomized? b.ne 0f bl kaslr_early_init // parse FDT for KASLR options cbz x0, 0f // KASLR disabled? just proceed orr x23, x23, x0 // record KASLR offset ldp x29, x30, [sp], #16 // we must enable KASLR, return ret // to __primary_switch() 0: #endif bl switch_to_vhe // Prefer VHE if possible ---(8) ldp x29, x30, [sp], #16 // ---(9) bl start_kernel // ---(10) ASM_BUG() SYM_FUNC_END(__primary_switched)
1. 为init进程(swapper进程)设置好堆栈地址和大小,保存当前进程描述符地址到sp_el0
在task_pt_regs(current)->stackframe创建一个最终帧记录,这样unwinder就可以根据任务堆栈中的位置来识别任何任务的最终帧记录。保留整个pt_regs空间使用户任务和kthread保持一致性。
/* * Initialize CPU registers with task-specific and cpu-specific context. * * Create a final frame record at task_pt_regs(current)->stackframe, so * that the unwinder can identify the final frame record of any task by * its location in the task stack. We reserve the entire pt_regs space * for consistency with user tasks and kthreads. */ .macro init_cpu_task tsk, tmp1, tmp2 msr sp_el0, \tsk ///init_task指针保存在sp_el0,因为当前运行在EL1异常等级,使用sp_el1来保存栈的指针,sp_el0闲置,内核空间中会使用sp_el0来作为current task struct的结构体 ldr \tmp1, [\tsk, #TSK_STACK] // 获取init_task的栈地址,offsetof(struct task_struct, stack) add sp, \tmp1, #THREAD_SIZE // 栈是由高地址向下生长的,所以SP_ELx要加上THREAD_SIZE sub sp, sp, #PT_REGS_SIZE // 为struct pt_regs留出空间 stp xzr, xzr, [sp, #S_STACKFRAME] // 将struct pt_regs的u64 stackframe[2]清零 add x29, sp, #S_STACKFRAME // x29(FP)指向栈中pt_regs的stackframe scs_load \tsk // 用于Clang Shadow Call Stack,此处为空操作 adr_l \tmp1, __per_cpu_offset // 读取__per_cpu_offset[NR_CPUS]数组基地址 ldr w\tmp2, [\tsk, #TSK_CPU] // offsetof(struct task_struct, cpu) ldr \tmp1, [\tmp1, \tmp2, lsl #3] // tmp1 = __per_cpu_offset[init_task.cpu << 3],通常来说,bootcpu为0 set_this_cpu_offset \tmp1 // 将当前cpu的per_cpu变量的offset值写入TPIDR_ELx .endm
几个寄存器的最终结果:
SP_EL0 = &init_task
SP_ELx = init_task.stack + THREAD_SIZE - sizeof(struct pt_regs)
x29(FP) = SP_ELx + S_STACKFRAME
2. 设置异常向量表基址寄存器
中断向量表的起始虚拟地址写入到VBAR_EL1。
3. 备份寄存器
此时sp的值为init_task.stack + THREAD_SIZE - sizeof(struct pt_regs)。主要工作如下:
- 将x29(FP)和x30(LR)分别保存到sp-16和sp-8的地址上,然后sp -= 16。
- 将sp的值写入到x29(FP)
这是实现了ARM64函数调用标准规定的栈布局,为后续函数调用的入栈出栈做好了准备。
4. 保存设备树物理地址到__fdt_pointer
x21寄存器的值是在preserve_boot_args接口中保存的FDT的地址。
5. 计算kimage_voffset
kimage_voffset记录了内核镜像映射后的虚拟地址与内核镜像在内存中的物理地址之间的差值。kimage_vaddr记录了_text的链接地址,也就是最终_text的虚拟地址,x0作为传入参数记录了_text的物理地址,相减即可得kimage_voffset。
6. 清空BSS段
7. 先初始化fixmap,然后通过fixmap为fdt建立页表(early_fdt_map)
early_fdt_map主要为KASLR服务,可能会失败,如果失败,会在setup_arch重新映射。
8. switch_to_vhe
在回一下异常等级初始化流程,在该流程中会通过hcr_el2.e2h判断是否会进入vhe模式,而这个标志是通过HCR_HOST_NVHE_FLAGS初始化的。因此若该标志未设置e2h位,则即使系统支持vhe也不会实际进入该模式。因为vhe模式的优势,故内核在这里会再给一次进入该模式的机会
9. 恢复x29(FP)和x30(LR)
从栈中恢复x29(FP)和x30(LR),sp重新指向init_task.stack + THREAD_SIZE - sizeof(struct pt_regs)。
10. 跳转start_kernel
离开头疼的汇编,看到我们熟悉的start_kernel。
一旦设定完了页表,那么打开MMU之后,kernel正式就会进入虚拟地址空间的世界,美中不足的是内核的虚拟世界没有那么大。原来拥有的整个物理地址空间都消失了,能看到的仅仅剩下kernel image mapping和identity mapping这两段地址空间是可见的。不过没有关系,这只是刚开始,内存初始化之路还很长。
本文来自博客园,作者:BSP-路人甲,转载请注明原文链接:https://www.cnblogs.com/jianhua1992/p/16842866.html,并保留此段声明,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理