MEMORY | INTERRUPT | TIMER | 并发与同步 | 进程管理 | 调度 | uboot | DTB | ARMV8 | ATF | Kernel Data Structure | PHY | LINUX2.6 | 驱动合集 | UART子系统 | USB专题 |

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这两段地址空间是可见的。不过没有关系,这只是刚开始,内存初始化之路还很长。

posted on 2022-11-21 22:19  BSP-路人甲  阅读(693)  评论(0编辑  收藏  举报

导航