ARM64启动过程分析

2023-05-16

文章目录

  • arm64启动过程分析
    • arm64启动过程分析(一)boot protocol
    • arm64启动过程分析(二)内核启动第一步
    • arm64启动过程分析(三)创建启动阶段页表
    • arm64启动过程分析(四)为开启mmu进行的cpu初始化
    • arm64启动过程分析(五)开启mmu
    • arm64启动过程分析(六)进入start_kernel
    • arm64启动过程分析(七)其他一些功能实现

arm64启动过程分析

参考:DDI0487F_b_armv8_arm.pdf

​ 蜗窝科技 ARMv8A Arch

arm64启动过程分析(一)boot protocol

arm64启动涉及到的实际内容较为复杂也多,这里分析主要在主流程上,使用的Linux版本为linux-5.0。

在系统启动过程中,首先由bootloader执行一系列操作,并最终将控制权交由kernel。这里的bootloader典型的是u-boot,也可能是Hypervisor和secure monitor,或者可能只是准备最小引导环境的少量指令。
kernel在引导阶段对bootloader提出以下要求:

  1. 设置并初始化RAM(必须)
  2. 准备好合适的设备树文件到RAM中,并提供dtb首地址给kernel(必须)
  3. 解压内核镜像(可选)
  4. 将控制权交由kernel(必须)
    在将控制权交由kernel时Image头部自身包含64-byte header信息,如下:
  u32 code0;			/* Executable code */
  u32 code1;			/* Executable code */
  u64 text_offset;		/* Image load offset, little endian */
  u64 image_size;		/* Effective Image size, little endian */
  u64 flags;			/* kernel flags, little endian */
  u64 res2	= 0;		/* reserved */
  u64 res3	= 0;		/* reserved */
  u64 res4	= 0;		/* reserved */
  u32 magic	= 0x644d5241;	/* Magic number, little endian, "ARM\x64" */
  u32 res5;			/* reserved (used for PE COFF offset) */

  1. code0/code1指向stext段,kernel执行代码的开始。
  2. 当支持EFI格式启动时,code0/code1将会被跳过。
    res5将是PE header偏移
  3. flags字段是小端的64bit 组合,包含如下信息:
    BIT 0:kernel字节序。1是BE,0是LE。
    BIT 1-2:kernel页大小。
    0 - 未指定
    1 - 4K
    2 - 16K
    3 - 64K
    BIT 3:kernel物理位置
    0 - 2MB对齐应尽可能接近RAM低地址底部,因为后面内存不能通过线性映射访问。
    1 - 2MB对齐可以在物理内存的任意位置。

在第四步控制权交给kernel时,对CPU状态,cache也有以下要求:

  1. 主CPU通用寄存器设置
    x0 = 设备树首地址的物理地址
    x1 = 0
    x2 = 0
    x3 = 0

  2. CPU模式
    所有中断都必须在PSTATE中被屏蔽,DAIF(debug,SError,IRQ和FIQ)
    CPU必须处于EL2(为了访问虚拟化拓展,建议使用EL2)或者非安全EL1中。

  3. Cache,MMUs
    MMU必须是关闭状态。
    Icache可以是关也可以是开。
    Dcache必须是关闭,这是为了保证加载的内核镜像的地址范围是clean to Poc的。

即如下设置:

The requirements are:   
  MMU = off, D-cache = off, I-cache = on or off,
  x0 = physical address to the FDT blob.

这里需要对data cache和instruction cache多说几句。我们知道,具体实现中的ARMv8处理器的cache是形成若干个level,一般而言,可能L1是分成了data cache和instruction cache,而其他level的cache都是unified cache。上面定义的D-cache off并不是说仅仅disable L1的data cache,实际上是disable了各个level的data cache和unified cache。同理,对于instruction cache亦然。

此外,在on/off控制上,MMU和data cache是有一定关联的。在ARM64中,SCTLR, System Control Register用来控制MMU icache和dcache,虽然这几个控制bit是分开的,但是并不意味着MMU、data cache、instruction cache的on/off控制是彼此独立的。一般而言,这里MMU和data cache是绑定的,即如果MMU 是off的,那么data cache也必须要off。因为如果打开data cache,那么要设定memory type、sharebility attribute、cachebility attribute等,而这些信息是保存在页表(Translation table)的描述符中,因此,如果不打开MMU,如果没有页表翻译过程,那么根本不知道怎么来应用data cache。当然,是不是说HW根本不允许这样设定呢?也不是了,在MMU OFF而data cache是ON的时候,这时候,所有的memory type和attribute是固定的,即memory type都是normal Non-shareable的,对于inner cache和outer cache,其策略都是Write-Back,Read-Write Allocate的。

  1. arch timers
    在所有CPU上,CNTFRQ必须设置好,CNTVOFF必须是关闭。

完成上述步骤即可将cpu控制权交由内核。

arm64启动过程分析(二)内核启动第一步

当内核开始接管cpu控制权后,会将设备树地址保存,并判断boot mode,并设置el2和el1中部分系统行为寄存器,并配置好虚拟化相关配置,以及异常行为等工作后会切换至内核工作的状态el1。

linux arm64启动代码位于arch/arm64/kernel/head.S,入口代码如下:

	/*
	 * The following callee saved general purpose registers are used on the
	 * primary lowlevel boot path:
	 *
	 *  Register   Scope                      Purpose
	 *  x21        stext() .. start_kernel()  FDT pointer passed at boot in x0
	 *  x23        stext() .. start_kernel()  physical misalignment/KASLR offset
	 *  x28        __create_page_tables()     callee preserved temp register
	 *  x19/x20    __primary_switch()         callee preserved temp registers
	 */
ENTRY(stext)
	bl	preserve_boot_args
	bl	el2_setup			// Drop to EL1, w0=cpu_boot_mode
	adrp	x23, __PHYS_OFFSET
	and	x23, x23, MIN_KIMG_ALIGN - 1	// KASLR offset, defaults to 0
	bl	set_cpu_boot_mode_flag
	bl	__create_page_tables
	/*
	 * The following calls CPU setup code, see arch/arm64/mm/proc.S for
	 * details.
	 * On return, the CPU will be ready for the MMU to be turned on and
	 * the TCR will have been set.
	 */
	bl	__cpu_setup			// initialise processor
	b	__primary_switch
ENDPROC(stext)

  1. preserve_boot_args
/*
 * Preserve the arguments passed by the bootloader in x0 .. x3
 */
preserve_boot_args:
	mov	x21, x0				// 将fdt首地址暂时存放至x21,释放x0用于其他使用

	adr_l	x0, boot_args			// x0保存boot_args变量地址
	stp	x21, x1, [x0]			// 这两步将x21,x1,x2,x3依次保存至boot_args中
	stp	x2, x3, [x0, #16]

	dmb	sy				// needed before dc ivac with
						// MMU off

	mov	x1, #0x20			// x0和x1是传递给__inval_cache_range的参数
	b	__inval_dcache_area		// tail call
ENDPROC(preserve_boot_args)

由于MMU=off,D-cache=off,因此写入boot_args变量的操作都是no cache的,直接写入sram中。为了安全起见(也许bootloader中打开了D-cache并操作了boot_args这段memory,从而在各个level的data cache和unified cache有了一些旧的,没有意义的数据),需要将boot_args变量对应的cache line进行清除并设置无效。在调用__inval_cache_range之前,x0是boot_args这段memory的首地址,x1是末尾的地址(boot_args变量长度是4x8byte=32byte,也就是0x20了)。
为何要保存x0~x3这四个寄存器呢?因为ARM64 boot protocol对启动时候的x0~x3这四个寄存器有严格的限制:x0是dtb的物理地址,x1~x3必须是0(非零值是保留将来使用)。在后续setup_arch函数执行的时候会访问boot_args并进行校验。

还有一个小细节是如何访问boot_args这个符号的,这个符号是一个虚拟地址,但是,现在没有建立好页表,也没有打开MMU,如何访问它呢?这是通过adr_l这个宏来完成的。这个宏实际上是通过adrp这个汇编指令完成,通过该指令可以将符号地址变成运行时地址(通过PC relative offset形式),因此,当运行的MMU OFF mode下,通过adrp指令可以获取符号的物理地址。不过adrp是page对齐的(adrp中的p就是page的意思),boot_args这个符号当然不会是page size对齐的,因此不能直接使用adrp,而是使用adr_l这个宏进行处理。

这里使用dmb sy指令,在armv8手册中说明:除了dc zva外,所有指定地址的数据缓存指令都可以按照任意顺序执行,在任何device属性地址,或者不可缓存的普通内存属性必须在指令之间执行dmb或者dsb保证顺序执行。

  1. el2_setup
    根据上面描述知道,cpu此时必须处于EL2或者EL1,这一段将会完成cpu对虚拟拓展和基本系统控制的设定,并最终将cpu退回至el1,此部分代码较长,分成四段。第一段如下:
/*
 * If we're fortunate enough to boot at EL2, ensure that the world is
 * sane before dropping to EL1.
 *
 * Returns either BOOT_CPU_MODE_EL1 or BOOT_CPU_MODE_EL2 in w0 if
 * booted in EL1 or EL2 respectively.
 */
ENTRY(el2_setup)
	msr	SPsel, #1			// We want to use SP_EL{1,2} --(1)
	mrs	x0, CurrentEL 		
	cmp	x0, #CurrentEL_EL2 ------- 判断当前cpu是否处于el2
	b.eq	1f ------------------- 如果是处于el2则跳转至往后标号1:处执行
	mov_q	x0, (SCTLR_EL1_RES1 | ENDIAN_SET_EL1) 
	msr	sctlr_el1, x0 -------------------------------------2)
	mov	w0, #BOOT_CPU_MODE_EL1		// This cpu booted in EL1
	isb
	ret

1:	mov_q	x0, (SCTLR_EL2_RES1 | ENDIAN_SET_EL2) ----------3)
	msr	sctlr_el2, x0

#ifdef CONFIG_ARM64_VHE
	/*
	 * Check for VHE being present. For the rest of the EL2 setup,
	 * x2 being non-zero indicates that we do have VHE, and that the
	 * kernel is intended to run at EL2.
	 */
	mrs	x2, id_aa64mmfr1_el1 --------------------------------4)
	ubfx	x2, x2, #8, #4
#else
	mov	x2, xzr -----------这里使用xzr而不是#0,其实是armv8架构pipe上的一种性能优化手段
#endif

	/* Hyp configuration. */
	mov_q	x0, HCR_HOST_NVHE_FLAGS
	cbz	x2, set_hcr -----------------------------------------5)
	mov_q	x0, HCR_HOST_VHE_FLAGS
set_hcr:
	msr	hcr_el2, x0
	isb

(1)设置SPsel bit0为1,允许使用sp_elx寄存器,否则只能使用sp_el0。
(2)当cpu处于el1时则无法配置虚拟化拓展相关内容则只需配置sctlr_el1后,并设置x0为BOOT_CPU_MODE_EL1后返回。
首先看看sctlr_el1寄存器定义:
SCTLR_EL1, System Control Register (EL1)
Provides top level control of the system, including its memory system, at EL1 and EL0.
SCTLR_EL1

sctlr_el1控制着整个系统行为。

SCTLR_EL1_RES1宏定义如下:(arch/arm64/include/asm/sysreg.h)

#define SCTLR_EL1_RES1	((_BITUL(11)) | (_BITUL(20)) | (_BITUL(22)) | (_BITUL(28)) | \
			 (_BITUL(29)))

这些BIT为预留BIT并且默认为1。

ENDIAN_SET_EL1宏定义如下:(arch/arm64/include/asm/sysreg.h)

#ifdef CONFIG_CPU_BIG_ENDIAN
#define ENDIAN_SET_EL1		(SCTLR_EL1_E0E | SCTLR_ELx_EE)
#define ENDIAN_CLEAR_EL1	0
#else
#define ENDIAN_SET_EL1		0
#define ENDIAN_CLEAR_EL1	(SCTLR_EL1_E0E | SCTLR_ELx_EE)
#endif

根据配置上述两个标志控制着系统大小端字节序,我们使用的都是小段故再次配置时el0和el1使用小端字节序。

综上可以知道当cpu处于el1阶段时则只配置cpu字节序为小端后则返回。

(3)同样的,当cpu处于el2时将el2,el1,el0配置为小端字节序。

(4)当配置了支持虚拟化拓展时,首先通过id_aa64mmfr1_el1寄存的VH字段获悉cpu是否支持Virtualization Host Extensions。

并将结果写入x2,如果没有配置则默认x2 = 0表示不支持此功能。

这里ubfx意思是从x2寄存器的第8bit开始提取4个bit数据并将结果写入x2。此字段对应VH feild。1表示cpu支持,0表示不支持。

(5)根据从id_aa64mmfr1_el1获取到的cpu是否对Virtualization Host Extensions提供支持来设置hcr_el2系统寄存器,该寄存器主要提供虚拟化控制配置以及陷入el2设置,定义如下:
HCR_EL2

HCR_HOST_NVHE_FLAGS 宏定义如下:(arch/arm64/include/asm/kvm_arm.h)

#define HCR_HOST_NVHE_FLAGS (HCR_RW | HCR_API | HCR_APK)

#define HCR_RW_SHIFT	31
#define HCR_RW		(UL(1) << HCR_RW_SHIFT) //设置1,el1执行状态为aarch64,el0执行状态由PSTATE值决定

#define HCR_API		(UL(1) << 41) //设置1,身份认证相关指令不会陷入el2

#define HCR_APK		(UL(1) << 40) //同上,认证相关’KEY‘值不会陷入el2

HCR_HOST_VHE_FLAGS宏定义如下:(arch/arm64/include/asm/kvm_arm.h)

#define HCR_HOST_VHE_FLAGS (HCR_RW | HCR_TGE | HCR_E2H)

#define HCR_TGE		(UL(1) << 27) //控制el0上异常捕捉相关,在el2安全状态激活时会对el0上某些指定捕捉并路由陷入到el2

#define HCR_E2H		(UL(1) << 34) //设置1,Host虚拟机操作系统运行在el2上被激活

最终根据x2的值判断是否支持虚拟化而设置hcr_el2系统寄存器,并同步指定执行。

第二段如下:

	/*
	 * Allow Non-secure EL1 and EL0 to access physical timer and counter.
	 * This is not necessary for VHE, since the host kernel runs in EL2,
	 * and EL0 accesses are configured in the later stage of boot process.
	 * Note that when HCR_EL2.E2H == 1, CNTHCTL_EL2 has the same bit layout
	 * as CNTKCTL_EL1, and CNTKCTL_EL1 accessing instructions are redefined
	 * to access CNTHCTL_EL2. This allows the kernel designed to run at EL1
	 * to transparently mess with the EL0 bits via CNTKCTL_EL1 access in
	 * EL2.
	 */
	cbnz	x2, 1f
	mrs	x0, cnthctl_el2
	orr	x0, x0, #3			// Enable EL1 physical timers
	msr	cnthctl_el2, x0 --------------------------------------------11:
	msr	cntvoff_el2, xzr		// 将虚拟计数counter清零保持与物理counter一致的计数值。

#ifdef CONFIG_ARM_GIC_V3 // 在允许cpu对gic v3直接访问时,配置cpu对gic v3的访问支持。
	/* GICv3 system register access */
	mrs	x0, id_aa64pfr0_el1
	ubfx	x0, x0, #24, #4 -----------------------------------------2)
	cbz	x0, 3f // 不支持对gic v3 cpu接口访问则跳过对gic v3的配置。

	mrs_s	x0, SYS_ICC_SRE_EL2
	orr	x0, x0, #ICC_SRE_EL2_SRE	// Set ICC_SRE_EL2.SRE==1 启用el1和el2访问ICH_*和ICC_*寄存器支持。
	orr	x0, x0, #ICC_SRE_EL2_ENABLE	// Set ICC_SRE_EL2.Enable==1 非安全el1访问ICC_SRE_EL1不会陷入el2。
	msr_s	SYS_ICC_SRE_EL2, x0
	isb					// Make sure SRE is now set
	mrs_s	x0, SYS_ICC_SRE_EL2		// Read SRE back,
	tbz	x0, #0, 3f			// and check that it sticks 检查设置情况
	msr_s	SYS_ICH_HCR_EL2, xzr		// Reset ICC_HCR_EL2 to defaults 若未成功设置,则复位ICH_HCR_EL2为0。

3:
#endif

	/* Populate ID registers. */
	mrs	x0, midr_el1 // 提供PE的定义信息和设备id号。
	mrs	x1, mpidr_el1 // 提供PE多处理器表示ID和分组等信息。
	msr	vpidr_el2, x0
	msr	vmpidr_el2, x1 // 这里将midr_el1和mpidr_el1的信息写入到了虚拟配置里供虚拟化使用。

#ifdef CONFIG_COMPAT
	msr	hstr_el2, xzr			// Disable CP15 traps to EL2 当配置支持aarch32时,兼容aarch32访问cp15不会陷入el2。
#endif

(1)当不支持虚拟化相关功能时,配置cnthctl_el2系统寄存器低两位为1表示非安全模式下el1和el0支持访问physical timer registers和physical counter register .

当支持虚拟化相关功能时,则是对el0的physical timer registers和physical counter register 访问配置,这里没有设置。

(2)id_aa64pfr0_el1寄存器主要提供对pe实现特性的一些信息。ubfx提取位是对gic支持的信息,为1表示支持系统寄存器在3.0/4.0版本的gic cpu接口访问。

第三段如下:

	/* EL2 debug */
	mrs	x1, id_aa64dfr0_el1		// Check ID_AA64DFR0_EL1 PMUVer 提供top level debug系统在aarch64的状态信息
	sbfx	x0, x1, #8, #4 // sbfx同理ubfx,u表示无符号,sbfx则是有符号位提供,此域提供对PMU支持情况信息
	cmp	x0, #1
	b.lt	4f				// Skip if no PMU present
	mrs	x0, pmcr_el0			// Disable debug access traps
	ubfx	x0, x0, #11, #5			// to EL2 and allow access to
4:
	csel	x3, xzr, x0, lt			// all PMU counters from EL1

	/* Statistical profiling */
	ubfx	x0, x1, #32, #4			// Check ID_AA64DFR0_EL1 PMSVer
	cbz	x0, 7f				// Skip if SPE not present
	cbnz	x2, 6f				// VHE?
	mrs_s	x4, SYS_PMBIDR_EL1		// If SPE available at EL2,
	and	x4, x4, #(1 << SYS_PMBIDR_EL1_P_SHIFT)
	cbnz	x4, 5f				// then permit sampling of physical
	mov	x4, #(1 << SYS_PMSCR_EL2_PCT_SHIFT | \
		      1 << SYS_PMSCR_EL2_PA_SHIFT)
	msr_s	SYS_PMSCR_EL2, x4		// addresses and physical counter
5:
	mov	x1, #(MDCR_EL2_E2PB_MASK << MDCR_EL2_E2PB_SHIFT)
	orr	x3, x3, x1			// If we don't have VHE, then
	b	7f				// use EL1&0 translation.
6:						// For VHE, use EL2 translation
	orr	x3, x3, #MDCR_EL2_TPMS		// and disable access from EL1
7:
	msr	mdcr_el2, x3			// Configure debug traps

	/* LORegions */
	mrs	x1, id_aa64mmfr1_el1
	ubfx	x0, x1, #ID_AA64MMFR1_LOR_SHIFT, 4
	cbz	x0, 1f
	msr_s	SYS_LORC_EL1, xzr
1:

	/* Stage-2 translation */
	msr	vttbr_el2, xzr // 将虚拟vttbr清空属于虚拟化注册功能组和内存虚拟化控制功能组相关内容。

	cbz	x2, install_el2_stub // 如果 支持了虚拟化则直接返回,后续在kvm配置虚拟化相关内容,并设置cpu boot在el2值保存在x0。

	mov	w0, #BOOT_CPU_MODE_EL2		// This CPU booted in EL2
	isb
	ret

install_el2_stub:

el2_setup最后一段:

install_el2_stub: // 这里最后在el2对el2和el1早期配置进行设置,并切换至el1。
	/*
	 * When VHE is not in use, early init of EL2 and EL1 needs to be
	 * done here.
	 * When VHE _is_ in use, EL1 will not be used in the host and
	 * requires no configuration, and all non-hyp-specific EL2 setup
	 * will be done via the _EL1 system register aliases in __cpu_setup.
	 */
	mov_q	x0, (SCTLR_EL1_RES1 | ENDIAN_SET_EL1)
	msr	sctlr_el1, x0 // 同处于el1一样,需要在el2时设置好sctlr_el1的值,主要配置大小端字节序。

	/* Coprocessor traps. */ //协处理器访问陷阱设置
	mov	x0, #0x33ff // 其中大部分位为预留值,主要配置CPACR,CPACR_EL1 SIMD访问时是否陷入el2,这里设置为都不陷入el2。
	msr	cptr_el2, x0			// Disable copro. traps to EL2

	/* SVE register access */ //可伸缩矢量拓展相关设置
	mrs	x1, id_aa64pfr0_el1
	ubfx	x1, x1, #ID_AA64PFR0_SVE_SHIFT, #4
	cbz	x1, 7f

	bic	x0, x0, #CPTR_EL2_TZ		// Also disable SVE traps
	msr	cptr_el2, x0			// Disable copro. traps to EL2
	isb
	mov	x1, #ZCR_ELx_LEN_MASK		// SVE: Enable full vector
	msr_s	SYS_ZCR_EL2, x1			// length for EL1.

	/* Hypervisor stub */
7:	adr_l	x0, __hyp_stub_vectors // __hyp_stub_vectors虚拟化管理异常向量表入口
	msr	vbar_el2, x0 // 将虚拟化管理异常向量表写入Vector Base Address Register el2

	/* spsr */
	mov	x0, #(PSR_F_BIT | PSR_I_BIT | PSR_A_BIT | PSR_D_BIT |\
		      PSR_MODE_EL1h) //mask Debug,SError,IRQ,FIQ,设置了spsr_el2的初始值
	msr	spsr_el2, x0
	msr	elr_el2, lr // 当使用eret指令时将会切换至低异常等级,,此时将返回地址写入异常返回指针elr_el2中
	mov	w0, #BOOT_CPU_MODE_EL2		// This CPU booted in EL2
	eret // 从此处返回后,cpu将工作在el1级别
ENDPROC(el2_setup)

  1. set_cpu_boot_mode_flag

完成el2_setup相关设置后cpu此时已经工作在el1等级上,并且x0保存了是从el1还是el2 boot的状态,如下:

/*
 * We need to find out the CPU boot mode long after boot, so we need to
 * store it in a writable variable.
 *
 * This is not in .bss, because we set it sufficiently early that the boot-time
 * zeroing of .bss would clobber it.
 */
ENTRY(__boot_cpu_mode)
	.long	BOOT_CPU_MODE_EL2 // .long 声明一组数组,每个数占用32位
	.long	BOOT_CPU_MODE_EL1
   
#define BOOT_CPU_MODE_EL1	(0xe11)
#define BOOT_CPU_MODE_EL2	(0xe12)

哪一个boot则会在对应地址写入0xe11或者0xe12。

接下来是set_cpu_boot_mode_flag,如下:

/*
 * Sets the __boot_cpu_mode flag depending on the CPU boot mode passed
 * in w0. See arch/arm64/include/asm/virt.h for more info.
 */
set_cpu_boot_mode_flag:
	adr_l	x1, __boot_cpu_mode
	cmp	w0, #BOOT_CPU_MODE_EL2
	b.ne	1f
	add	x1, x1, #4
1:	str	w0, [x1]			// This CPU has booted in EL1
	dmb	sy
	dc	ivac, x1			// Invalidate potentially stale cache line
	ret
ENDPROC(set_cpu_boot_mode_flag)

这个段功能主要是如上面描述通过x0保存的值判断cpu boot时是从el1还是el2,当是从el2 boot则将x0的值写入变量__boot_cpu_mode的第一个地址处,如果是el1 boot则将x0的值写入变量__boot_cpu_mode + 4地址处。

arm64启动过程分析(三)创建启动阶段页表

当从bootloader进入kernel时根据protocol规定mmu和d-cahe是关闭,为了提供性能,加快初始化速度,内核需要在某个阶段尽快开启mmu和cache,而在开启之前必须先设定好页表。

通常情况下,按照传统来arm64的设定是4K页表,48bit位的设定,此部分也是按照这个设定来进行分析。

关于mmu和cache相关详细描述可以参考蜗窝科技:ARM64的启动过程之(二):创建启动阶段的页表。

在启动阶段主要有三个页表需要建立,第一个是idmap_pg_dir,这个建立一致性映射,也就是为了从物理地址平滑切换至虚拟地址时所做的映射段,第二个是init_pg_dir,这个建立整个内核镜像的映射包括text段,data,rodata,bss等。这样后续所有内核代码可以正常运行。第三个是swapper_pg_dir,这个主要是在boot从cpu激活mmu时及后续fixmap用的映射,这里我们不用关心。

在arm64内核配置中kernel支持4K和64K,3级及4级页表配置,如下:

AArch64 Linux memory layout with 4KB pages + 3 levels:

Start			End			Size		Use
-----------------------------------------------------------------------
0000000000000000	0000007fffffffff	 512GB		user
ffffff8000000000	ffffffffffffffff	 512GB		kernel


AArch64 Linux memory layout with 4KB pages + 4 levels:

Start			End			Size		Use
-----------------------------------------------------------------------
0000000000000000	0000ffffffffffff	 256TB		user
ffff000000000000	ffffffffffffffff	 256TB		kernel


AArch64 Linux memory layout with 64KB pages + 2 levels:

Start			End			Size		Use
-----------------------------------------------------------------------
0000000000000000	000003ffffffffff	   4TB		user
fffffc0000000000	ffffffffffffffff	   4TB		kernel


AArch64 Linux memory layout with 64KB pages + 3 levels:

Start			End			Size		Use
-----------------------------------------------------------------------
0000000000000000	0000ffffffffffff	 256TB		user
ffff000000000000	ffffffffffffffff	 256TB		kernel

    
Translation table lookup with 4KB pages:

+--------+--------+--------+--------+--------+--------+--------+--------+
|63    56|55    48|47    40|39    32|31    24|23    16|15     8|7      0|
+--------+--------+--------+--------+--------+--------+--------+--------+
 |                 |         |         |         |         |
 |                 |         |         |         |         v
 |                 |         |         |         |   [11:0]  in-page offset
 |                 |         |         |         +-> [20:12] L3 index
 |                 |         |         +-----------> [29:21] L2 index
 |                 |         +---------------------> [38:30] L1 index
 |                 +-------------------------------> [47:39] L0 index
 +-------------------------------------------------> [63] TTBR0/1


Translation table lookup with 64KB pages:

+--------+--------+--------+--------+--------+--------+--------+--------+
|63    56|55    48|47    40|39    32|31    24|23    16|15     8|7      0|
+--------+--------+--------+--------+--------+--------+--------+--------+
 |                 |    |               |              |
 |                 |    |               |              v
 |                 |    |               |            [15:0]  in-page offset
 |                 |    |               +----------> [28:16] L3 index
 |                 |    +--------------------------> [41:29] L2 index
 |                 +-------------------------------> [47:42] L1 index
 +-------------------------------------------------> [63] TTBR0/1

我们使用48bit位和4K页表则共需要PGD,PUD,PMD,PTE四级。页表创建第一段代码如下:

/*
 * Setup the initial page tables. We only setup the barest amount which is
 * required to get the kernel running. The following sections are required:
 *   - identity mapping to enable the MMU (low address, TTBR0)
 *   - first few MB of the kernel linear mapping to jump to once the MMU has
 *     been enabled
 */
__create_page_tables:
	mov	x28, lr

	/*
	 * Invalidate the init page tables to avoid potential dirty cache lines
	 * being evicted. Other page tables are allocated in rodata as part of
	 * the kernel image, and thus are clean to the PoC per the boot
	 * protocol.
	 */
	adrp	x0, init_pg_dir 
	adrp	x1, init_pg_end
	sub	x1, x1, x0  			// 首先无效化init_pg_dir所在的cache line。
	bl	__inval_dcache_area

	/*
	 * Clear the init page tables.
	 */
	adrp	x0, init_pg_dir
	adrp	x1, init_pg_end
	sub	x1, x1, x0
1:	stp	xzr, xzr, [x0], #16
	stp	xzr, xzr, [x0], #16
	stp	xzr, xzr, [x0], #16
	stp	xzr, xzr, [x0], #16
	subs	x1, x1, #64 // 循环清空pgd init_pg_dir
	b.ne	1b

	mov	x7, SWAPPER_MM_MMUFLAGS --------------------------------------1

(1)SWAPPER_MM_MMUFLAGS宏定义如下:(arch/arm64/include/asm/kernel-pgtable.h)

/*
 * The linear mapping and the start of memory are both 2M aligned (per
 * the arm64 booting.txt requirements). Hence we can use section mapping
 * with 4K (section size = 2M) but not with 16K (section size = 32M) or
 * 64K (section size = 512M).
 */
#ifdef CONFIG_ARM64_4K_PAGES
#define ARM64_SWAPPER_USES_SECTION_MAPS 1 --------------------------------------------(2)
#else
#define ARM64_SWAPPER_USES_SECTION_MAPS 0
#endif

#if ARM64_SWAPPER_USES_SECTION_MAPS
#define SWAPPER_MM_MMUFLAGS	(PMD_ATTRINDX(MT_NORMAL) | SWAPPER_PMD_FLAGS) -------------(3)
#else
#define SWAPPER_MM_MMUFLAGS	(PTE_ATTRINDX(MT_NORMAL) | SWAPPER_PTE_FLAGS)
#endif

/*
 * Initial memory map attributes.
 */
#define SWAPPER_PTE_FLAGS	(PTE_TYPE_PAGE | PTE_AF | PTE_SHARED)	-------------------------(4)
#define SWAPPER_PMD_FLAGS	(PMD_TYPE_SECT | PMD_SECT_AF | PMD_SECT_S)  --------------------(4)

(2)当我们使用4K page时则映射时可使用段映射也就是说,当对内核进行映射时可在pmd设置页表属性为block pagetable这样就可以提前结束地址翻译并使每一个条目寻址可达2M方便内核映射。

(3)内核使用的几种memory属性如下:

/*
 * Memory types available.
 */
#define MT_DEVICE_nGnRnE	0 --------------------------- 0x00
#define MT_DEVICE_nGnRE		1 --------------------------- 0x04
#define MT_DEVICE_GRE		2 --------------------------- 0x0c
#define MT_NORMAL_NC		3 --------------------------- 0x44
#define MT_NORMAL		4	--------------------------- 0xff
#define MT_NORMAL_WT		5 --------------------------- 0xbb (inner/outer Write-Through)

MT_DEVICE_*属性的不会经过cache并可设置G,R,E相关属性具体同样参考上面链接。最终上述几种属性在页表中通过页表attr[4,2]来设置访问。

(4)SWAPPER_PTE_FLAGS定义为#define PMD_TYPE_SECT (_AT(pmdval_t, 1) << 0)也就是说页表的bit0设置为1 bit1为0表示此页表可用并且为block pagetable(bit1为1是Table descriptor)所以当使用段映射内核时在PMD设置页表为block pagetable则后面不需要pte表,所以在vmlinux.lds.S中idmap_pg_dir只分配了三个页表即可。而SWAPPER_PTE_FLAGS定义正好是bit1 为1表示

PTE_AF和PMD_SECT_AF都是指向bit10为1 access flag的意思表示该memory block(或者page)是否被最近被访问过。当然,这需要软件的协助。如果该bit被设置为0,当程序第一次访问的时候会产生异常,软件需要将给bit设置为1,之后再访问该page的时候,就不会产生异常了。不过当软件认为该page已经old enough的时候,也可以clear这个bit,表示最近都没有访问该page。这个flag是硬件对page reclaim算法的支持,找到最近不常访问的那些page。当然在这个场景下,我们没有必要enable这个特性,因此将其设定为1。

PTE_SHARED和PMD_SECT_S都是指向bit8 bit9为3表示inner/outer都为Write-Back Cacheable。

至此SWAPPER_MM_MMUFLAGS在这里就表示完了,这个值会写入x7寄存器并在map_memory时写入block pagetable对应位置以设置block描述符属性。

地址翻译描述:

在armv8中地址翻译分为stage 1和stage 2,页表分为Table descriptor,Block descriptor,Page descriptor。

在页表翻译中有以下几种同步异常可以产生

  • Translation fault.
  • Access flag fault.
  • Permission fault.
  • Domain fault, when translating using the AArch32 translation systems.
  • Address size fault.
  • TLB conflict fault.
  • Synchronous External aborts during a translation table walk.
  • 及附加的fault(不关心)

由上面知道armv8支持3级和4级页表,当在pgd和pud或者pmd(level 0,level 1)此时称为Table descriptor它会指向下一级页表。当在pmd或者pte(level 2,level 1)时通过bit 1设置,bit 1为1表示为page descriptor它指向一个页大小的内存,bit 1为0时表示Block descriptor它指向一个内存块,在4k分页时为2M大小。详细描述如下:

一般情况下描述符是以下其中之一:

  • An invalid or fault entry.
  • A table entry, that points to the next-level translation table.
  • A block entry, that defines the memory properties for the access.
  • A reserved format.

bit[1]用于描述descriptor type,bit[0]用于描述descriptor是否可用。

根据颗粒粒度不同可细分为以下几种描述方式:

4KB粒度 level 0不能作为Block descriptors.

  • 在level 1可输出1GB范围地址映射

  • 在level 2可输出2M范围地址映射

此时OA size为 48 bit.

16KB粒度 level 0和level 1不能作为Block descriptors.

  • 在 level 2可输出32MB范围地址映射

此时OA size为 48 bit.

64KB粒度 level 0不支持并且此情况需要根据Large PA and IPA support 情况来判定,ID_AA64MMFR0_EL1可获取支持情况

LPA支持下

  • level 1可输出4TB范围地址映射
  • level 2可输出512MB范围地址映射

此时OA size为 48 bit.

LPA不支持下,level 1不支持

  • level 2可输出512MB范围地址映射

此时OA size为 48 bit.

level 0,level 1,level 2 descriptor formats:

LEVEL_DESC

当bit[0]为0时描述符不可用。

当bit[0]为1,bit[1]为0时,表示为Block 描述符。

当bit[0]为1,bit[1]为1时,表示为Table 描述符。

level 3 descriptor formats:
PAGE_DESC

当bit[0]为0时描述符不可用。

当bit[0]为1,bit[1]为0时,表示为无效预留描述符。

当bit[0]为1,bit[1]为1时,表示为Page 描述符。

Table descriptor 字段定义,stage 1:
TABLE_DEFINE

Block and Page descriptor 字段定义,stage 1:

PAGE_DEFINE

Block and Page descriptor 字段定义,stage 2:
PAGE_DEFINE2

关于stage 1和stage 2描述可以参考armv8虚拟内存架构简述。

经过漫长SWAPPER_MM_MMUFLAGS定义描述,下面开始描述页表创建的第二段:

	/*
	 * Create the identity mapping.
	 */
	adrp	x0, idmap_pg_dir
	adrp	x3, __idmap_text_start		// __pa(__idmap_text_start) ------------- (1)

#ifdef CONFIG_ARM64_USER_VA_BITS_52 // 未分析此配置
	mrs_s	x6, SYS_ID_AA64MMFR2_EL1
	and	x6, x6, #(0xf << ID_AA64MMFR2_LVA_SHIFT)
	mov	x5, #52
	cbnz	x6, 1f
#endif
	mov	x5, #VA_BITS
1:
	adr_l	x6, vabits_user
	str	x5, [x6] // 将使用多少位bit VA_BITS保存在变量vabits_user里。
	dmb	sy
	dc	ivac, x6		// Invalidate potentially stale cache line

	/*
	 * VA_BITS may be too small to allow for an ID mapping to be created
	 * that covers system RAM if that is located sufficiently high in the
	 * physical address space. So for the ID map, use an extended virtual
	 * range in that case, and configure an additional translation level
	 * if needed.
	 *
	 * Calculate the maximum allowed value for TCR_EL1.T0SZ so that the
	 * entire ID map region can be mapped. As T0SZ == (64 - #bits used),
	 * this number conveniently equals the number of leading zeroes in
	 * the physical address of __idmap_text_end.
	 */
	adrp	x5, __idmap_text_end
	clz	x5, x5
	cmp	x5, TCR_T0SZ(VA_BITS)	// default T0SZ small enough? 这里用于获取t0sz大小是否满足当前映射
	b.ge	1f			// .. then skip VA range extension

	adr_l	x6, idmap_t0sz
	str	x5, [x6]
	dmb	sy
	dc	ivac, x6		// Invalidate potentially stale cache line

#if (VA_BITS < 48) // 未分析不是BIT 48情况
#define EXTRA_SHIFT	(PGDIR_SHIFT + PAGE_SHIFT - 3)
#define EXTRA_PTRS	(1 << (PHYS_MASK_SHIFT - EXTRA_SHIFT))

	/*
	 * If VA_BITS < 48, we have to configure an additional table level.
	 * First, we have to verify our assumption that the current value of
	 * VA_BITS was chosen such that all translation levels are fully
	 * utilised, and that lowering T0SZ will always result in an additional
	 * translation level to be configured.
	 */
#if VA_BITS != EXTRA_SHIFT
#error "Mismatch between VA_BITS and page size/number of translation levels"
#endif

	mov	x4, EXTRA_PTRS
	create_table_entry x0, x3, EXTRA_SHIFT, x4, x5, x6
#else
	/*
	 * If VA_BITS == 48, we don't have to configure an additional
	 * translation level, but the top-level table has more entries.
	 */
	mov	x4, #1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT)
	str_l	x4, idmap_ptrs_per_pgd, x5
#endif
1: -----------------------------------------------------------------------2

(1)这里将__idmap_text_start的物理地址取出,并在map_memory中将__idmap_text_start的虚拟地址设置为和物理地址一致建立一致性映射。

(2)当我们使用48bit并满足映射条件时,会跳转至此标号开始map_memory

页表创建的最后一段:

1:
	ldr_l	x4, idmap_ptrs_per_pgd
	mov	x5, x3				// __pa(__idmap_text_start)
	adr_l	x6, __idmap_text_end		// __pa(__idmap_text_end)

	map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14 ----------------1/*
	 * Map the kernel image (starting with PHYS_OFFSET).
	 */
	adrp	x0, init_pg_dir
	mov_q	x5, KIMAGE_VADDR + TEXT_OFFSET	// compile time __va(_text)
	add	x5, x5, x23			// add KASLR displacement
	mov	x4, PTRS_PER_PGD
	adrp	x6, _end			// runtime __pa(_end)
	adrp	x3, _text			// runtime __pa(_text)
	sub	x6, x6, x3			// _end - _text
	add	x6, x6, x5			// runtime __va(_end)

	map_memory x0, x1, x5, x6, x7, x3, x4, x10, x11, x12, x13, x14 ----------------1/*
	 * Since the page tables have been populated with non-cacheable
	 * accesses (MMU disabled), invalidate the idmap and swapper page
	 * tables again to remove any speculatively loaded cache lines.
	 */
	adrp	x0, idmap_pg_dir
	adrp	x1, init_pg_end
	sub	x1, x1, x0
	dmb	sy
	bl	__inval_dcache_area

	ret	x28 ----------------------------------------------------------------------3ENDPROC(__create_page_tables)
	.ltorg

(1)此处完成一致性idmap_pg_dir映射,具体映射算法可参见(arch/arm64/kernel/head.S)中定义宏。带入寄存器参数描述:

/*
 * Map memory for specified virtual address range. Each level of page table needed supports
 * multiple entries. If a level requires n entries the next page table level is assumed to be
 * formed from n pages.
 *
 *	tbl:	location of page table
 *	rtbl:	address to be used for first level page table entry (typically tbl + PAGE_SIZE)
 *	vstart:	start address to map
 *	vend:	end address to map - we map [vstart, vend]
 *	flags:	flags to use to map last level entries
 *	phys:	physical address corresponding to vstart - physical memory is contiguous
 *	pgds:	the number of pgd entries
 *
 * Temporaries:	istart, iend, tmp, count, sv - these need to be different registers
 * Preserves:	vstart, vend, flags
 * Corrupts:	tbl, rtbl, istart, iend, tmp, count, sv
 */
.macro map_memory, tbl, rtbl, vstart, vend, flags, phys, pgds, istart, iend, tmp, count, sv
 
idmap_pg_dir map:
map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14
x0 idmap_pg_dir (phy)
x1 init_pg_end (phy)
x5 __idmap_text_start (phy)
x6 __idmap_text_end (phy)
x7 SWAPPER_MM_MMUFLAGS (flags)
x3 __idmap_text_start (phy)
x4 idmap_ptrs_per_pgd (页表条目数)
x10
x11
x12
x13
x14
    

(2)此处完成内核映射,具体映射算法可参见(arch/arm64/kernel/head.S)中定义宏。带入寄存器参数描述:

map_memory x0, x1, x5, x6, x7, x3, x4, x10, x11, x12, x13, x14
x0 init_pg_dir (phy)
x1 
x5 _text (virt)
x6 _end (virt)
x7 SWAPPER_MM_MMUFLAGS (flags)
x3 _text (phy)
x4 PTRS_PER_PGD
x10
x11
x12
x13
x14

(3)完成上述两段映射后即可返回继续后续cpu初始化并开启mmu和cache。

此处有一个.ltorg伪指令,具体描述可参考LTORG的几点理解。

arm64启动过程分析(四)为开启mmu进行的cpu初始化

在设置好页表后,需要对tcr_el1进行配置以便开启mmu,首先是代码:

/*
 *	__cpu_setup
 *
 *	Initialise the processor for turning the MMU on.  Return in x0 the
 *	value of the SCTLR_EL1 register.
 */
	.pushsection ".idmap.text", "awx"
ENTRY(__cpu_setup)
	tlbi	vmalle1				// Invalidate local TLB --------------------------------(1)
	dsb	nsh

	mov	x0, #3 << 20
	msr	cpacr_el1, x0			// Enable FP/ASIMD ------------------------------------(2)
	mov	x0, #1 << 12			// Reset mdscr_el1 and disable
	msr	mdscr_el1, x0			// access to the DCC from EL0 --------------------------(3)
	isb					// Unmask debug exceptions now,
	enable_dbg				// since this is per-cpu -----------------------------------(4)
	reset_pmuserenr_el0 x0			// Disable PMU access from EL0 ---------------------(5)
	/*
	 * Memory region attributes for LPAE:
	 *
	 *   n = AttrIndx[2:0]
	 *			n	MAIR
	 *   DEVICE_nGnRnE	000	00000000
	 *   DEVICE_nGnRE	001	00000100
	 *   DEVICE_GRE		010	00001100
	 *   NORMAL_NC		011	01000100
	 *   NORMAL		100	11111111
	 *   NORMAL_WT		101	10111011
	 */
	ldr	x5, =MAIR(0x00, MT_DEVICE_nGnRnE) | \
		     MAIR(0x04, MT_DEVICE_nGnRE) | \
		     MAIR(0x0c, MT_DEVICE_GRE) | \
		     MAIR(0x44, MT_NORMAL_NC) | \
		     MAIR(0xff, MT_NORMAL) | \
		     MAIR(0xbb, MT_NORMAL_WT) ------------------------------------------------6)
	msr	mair_el1, x5
	/*
	 * Prepare SCTLR
	 */
	mov_q	x0, SCTLR_EL1_SET --------------------------------------------------------7/*
	 * Set/prepare TCR and TTBR. We use 512GB (39-bit) address range for
	 * both user and kernel.
	 */
	ldr	x10, =TCR_TxSZ(VA_BITS) | TCR_CACHE_FLAGS | TCR_SMP_FLAGS | \
			TCR_TG_FLAGS | TCR_KASLR_FLAGS | TCR_ASID16 | \
			TCR_TBI0 | TCR_A1 | TCR_KASAN_FLAGS --------------------------------------8#ifdef CONFIG_ARM64_USER_VA_BITS_52 // 未使用未分析
	ldr_l		x9, vabits_user
	sub		x9, xzr, x9
	add		x9, x9, #64
#else
	ldr_l		x9, idmap_t0sz
#endif
	tcr_set_t0sz	x10, x9 // tcr_set_t0sz是一个宏作用是将x9中低6位放入x10低六位,也就是更新VA_BITS的值。

	/*
	 * Set the IPS bits in TCR_EL1.
	 */
	tcr_compute_pa_size x10, #TCR_IPS_SHIFT, x5, x6 -----------------------------------9#ifdef CONFIG_ARM64_HW_AFDBM ----------------------------------------------------------(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	x10, x10, #TCR_HA		// hardware Access flag update
1:
#endif	/* CONFIG_ARM64_HW_AFDBM */
	msr	tcr_el1, x10 ------------------------------------------------------------------11)
	ret					// return to head.S
ENDPROC(__cpu_setup)

(1)无效化本cpu所有条目tlbs,包括Global entries和Non-global entries with any ASID。

(2)与SIMD相关配置,当执行浮点计算相关指令时不会陷入el1。

CACAR_EL1

(3)支持从el0访问DCC寄存器

DDC

(4)enable_dbg是一个宏功能就是把DAIF中D清除以支持debug功能。

	.macro	enable_dbg
	msr	daifclr, #8
	.endm

(5)同样的,关闭从el0访问pmu相关寄存器,访问会陷入el1。

(6)此表在第三部分讲述了含义,页表通过attr[2:0]来访问memory属性,如下:

MAIR

(7)SCTLR_EL1_SET这是一个预备的sctlr_el1值会在后续设置,sctlr_el1定义在第二章已有,SCTLR_EL1_SET定义如下:

(arch/arm64/include/asm/sysreg.h)

#define SCTLR_EL1_SET	(SCTLR_ELx_M    | SCTLR_ELx_C    | SCTLR_ELx_SA   |\
			 SCTLR_EL1_SA0  | SCTLR_EL1_SED  | SCTLR_ELx_I    |\
			 SCTLR_EL1_DZE  | SCTLR_EL1_UCT                   |\
			 SCTLR_EL1_NTWE | SCTLR_ELx_IESB | SCTLR_EL1_SPAN |\
			 ENDIAN_SET_EL1 | SCTLR_EL1_UCI  | SCTLR_EL1_RES1)

#define SCTLR_ELx_M	(_BITUL(0)) // 激活mmu
#define SCTLR_ELx_C	(_BITUL(2)) // 激活D-cahce
#define SCTLR_ELx_SA	(_BITUL(3)) // 激活el1 sp对齐检查(16byte对齐)
#define SCTLR_EL1_SA0		(_BITUL(4)) // 激活el0 sp对齐检查(16byte对齐)
#define SCTLR_EL1_SED		(_BITUL(8)) // 在el0不能使用aarch32指令
#define SCTLR_ELx_I	(_BITUL(12)) // 激活I-cache
#define SCTLR_EL1_DZE		(_BITUL(14)) // 允许el0使用dc zva
#define SCTLR_EL1_UCT		(_BITUL(15)) // 允许el0访问ctr_el0
#define SCTLR_EL1_NTWE		(_BITUL(18)) // 允许el0使用wfe指令
#define SCTLR_ELx_IESB	(_BITUL(21)) // 隐式访问同步错误事件激活
#define SCTLR_EL1_SPAN		(_BITUL(23)) // 在el1异常时,PSTATE.PAN的值不变

#ifdef CONFIG_CPU_BIG_ENDIAN
#define ENDIAN_SET_EL1		(SCTLR_EL1_E0E | SCTLR_ELx_EE) //el1和el0大小端设置
#define ENDIAN_CLEAR_EL1	0
#else
#define ENDIAN_SET_EL1		0
#define ENDIAN_CLEAR_EL1	(SCTLR_EL1_E0E | SCTLR_ELx_EE)
#endif

#define SCTLR_EL1_UCI		(_BITUL(26)) // el0使用高速缓存指令不会陷入el1。

#define SCTLR_EL1_RES1	((_BITUL(11)) | (_BITUL(20)) | (_BITUL(22)) | (_BITUL(28)) | \
			 (_BITUL(29))) // 预留值

(8)此配置是对mmu等相关的配置tcr_el1,描述如下:
BLOCK
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
链机制,建议将图片保存下来直接上传(img-1pwPCcMz-1609149126172)(./tcr_el1.png)]

TCR_TxSZ(VA_BITS) 根据VA_BSTS值设置T0SZ和T1SZ设置虚拟内存寻址2^(48) TXSZ = 64 - VA_BITS
TCR_CACHE_FLAGS 设置IRGN0和IRGN1,IRGN0和IRGN1,TLB访问属性为Normal memory, Outer Write-Back Read-Allocate Write-Allocate Cacheable,Normal memory, Inner Write-Back Read-Allocate Write-Allocate Cacheable。
TCR_SMP_FLAGS 共享内存属性配置为Inner Shareable。不与其他PE共享。

#ifdef CONFIG_ARM64_64K_PAGES
#define TCR_TG_FLAGS	TCR_TG0_64K | TCR_TG1_64K
#elif defined(CONFIG_ARM64_16K_PAGES)
#define TCR_TG_FLAGS	TCR_TG0_16K | TCR_TG1_16K
#else /* CONFIG_ARM64_4K_PAGES */
#define TCR_TG_FLAGS	TCR_TG0_4K | TCR_TG1_4K
#endif
根据4K,16K,64K,来配置TG0,TG1。
    
TCR_KASLR_FLAGS随机化配置支持 BIT54默认未开启。

TCR_ASID16 设置ASID为16bit。
    
TCR_TBI0 忽略TTBR0_EL1中top bit使其用于应用使用。

TCR_A1 TTBR1_EL1.ASID定义

TCR_KASAN_FLAGS top btye忽略在ttbr1_el1此功能与是否配置CONFIG_KASAN_SW_TAGS有关
  

(9)tcr_compute_pa_size宏定义如下:(arch/arm64/include/asm/assembler.h)

/*
 * 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
	mrs	\tmp0, ID_AA64MMFR0_EL1
	// Narrow PARange to fit the PS field in TCR_ELx
	ubfx	\tmp0, \tmp0, #ID_AA64MMFR0_PARANGE_SHIFT, #3
	mov	\tmp1, #ID_AA64MMFR0_PARANGE_MAX
	cmp	\tmp0, \tmp1
	csel	\tmp0, \tmp1, \tmp0, hi
	bfi	\tcr, \tmp0, \pos, #3
	.endm
        

这个宏作用是根据配置最大VA_BITS和cpu支持的最大物理地址来获取当前配置使用的支持物理地址大小并将这个设置值写如tcr_el1的IPS位。

(10)根据硬件是否支持更新页表中af标志位,来设置tcr_el1中的HA位,以此支持在stage 1阶段可以自动更新af标志。

(11)至此便完成了翻译控制tcr_el1相关的设置并将设置好的x10值写入tcr_el1并返回head.S中去准备开启mmu。

arm64启动过程分析(五)开启mmu

在完成页表翻译相关设置后,就是需要开启mmu,这也是进入start_kernel最后一步,代码如下:

__primary_switch:
#ifdef CONFIG_RANDOMIZE_BASE // 随机化地址未分析
	mov	x19, x0				// preserve new SCTLR_EL1 value
	mrs	x20, sctlr_el1			// preserve old SCTLR_EL1 value
#endif

	adrp	x1, init_pg_dir // 已知x0为sctlr_el1预设好的值,x1为init_pg_dir pgd的物理基地址。
	bl	__enable_mmu --------------------------------------------------------------1#ifdef CONFIG_RELOCATABLE // 未使用未分析
	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

	msr	sctlr_el1, x19			// re-enable the MMU
	isb
	ic	iallu				// flush instructions fetched
	dsb	nsh				// via old mapping
	isb

	bl	__relocate_kernel
#endif
#endif
	ldr	x8, =__primary_switched
	adrp	x0, __PHYS_OFFSET
	br	x8 // 跳转至__primary_switched,进行最后栈设置及异常向量表设置后进入start_kernel。
ENDPROC(__primary_switch)

(1)__enable_mmu,代码如下:

/*
 * Enable the MMU.
 *
 *  x0  = SCTLR_EL1 value for turning on the MMU.
 *  x1  = TTBR1_EL1 value
 *
 * 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
 */
ENTRY(__enable_mmu)
	mrs	x2, ID_AA64MMFR0_EL1
	ubfx	x2, x2, #ID_AA64MMFR0_TGRAN_SHIFT, 4
	cmp	x2, #ID_AA64MMFR0_TGRAN_SUPPORTED // 判断ID_AA64MMFR0_EL1中相应的TGRAN是否支持设置的4K粒度。
	b.ne	__no_granule_support // 如果不支持,那cpu会进入wfe,wfi状态并死循环。
	update_early_cpu_boot_status 0, x2, x3 // 未理解这个操作,把0值写入一个变量中。
	adrp	x2, idmap_pg_dir // 将x2设置为idmap_pg_dir pgd的物理基地址。
	phys_to_ttbr x1, x1 // phys_to_ttbr这个操作因为如果是52bit的需要进行一个转换后才能写入ttbr中,在48bit这个x1的值没有变化。
	phys_to_ttbr x2, x2
	msr	ttbr0_el1, x2			// load TTBR0 将idmap_pg_dir pgd写入ttbr0_el1
	offset_ttbr1 x1
	msr	ttbr1_el1, x1			// load TTBR1 将init_pg_dir pgd写入ttbr1_el1
	isb
	msr	sctlr_el1, x0 // 将预设的系统行为控制寄存器值x0写入sctlr_el1,在这一步前后都有同步指令及清I-cahce操作,之后便是虚拟地址世界。
	isb
	/*
	 * Invalidate the local I-cache so that any instructions fetched
	 * speculatively from the PoC are discarded, since they may have
	 * been dynamically patched at the PoU.
	 */
	ic	iallu
	dsb	nsh
	isb
	ret
ENDPROC(__enable_mmu)

arm64启动过程分析(六)进入start_kernel

在第五部分开启mmu后,使用bx命令跳转至__primary_switched,进行栈设置和异常向量表设置后进入start_kernel,代码如下:

/*
 * The following fragment of code is executed with the MMU enabled.
 *
 *   x0 = __PHYS_OFFSET
 */
__primary_switched:
	adrp	x4, init_thread_union
	add	sp, x4, #THREAD_SIZE
	adr_l	x5, init_task
	msr	sp_el0, x5			// Save thread_info ----------------------------------------------(1)

	adr_l	x8, vectors			// load VBAR_EL1 with virtual
	msr	vbar_el1, x8			// vector table address --------------------------------------(2)
	isb

	stp	xzr, x30, [sp, #-16]!
	mov	x29, sp ------------------------------------------------------------------------------3)

	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
	adr_l	x0, __bss_start
	mov	x1, xzr
	adr_l	x2, __bss_stop
	sub	x2, x2, x0
	bl	__pi_memset ---------------------------------------------------------------------------6)
	dsb	ishst				// Make zero page visible to PTW

#ifdef CONFIG_KASAN // 未使用为分析
	bl	kasan_early_init
#endif
#ifdef CONFIG_RANDOMIZE_BASE // 未使用为分析
	tst	x23, ~(MIN_KIMG_ALIGN - 1)	// already running randomized?
	b.ne	0f
	mov	x0, x21				// pass FDT address in x0
	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
	add	sp, sp, #16
	mov	x29, #0
	mov	x30, #0
	b	start_kernel ------------------------------------------------------------------------7ENDPROC(__primary_switched)

(1)内核使用的stack接用了init_task的栈空间也就是thread_info部分定义如下:

/*
 * Set up the first task table, touch at your own risk!. Base=0,
 * limit=0x1fffff (=2MB)
 */
struct task_struct init_task
#ifdef CONFIG_ARCH_TASK_STRUCT_ON_STACK
	__init_task_data
#endif
= {
#ifdef CONFIG_THREAD_INFO_IN_TASK // 已定义
	.thread_info	= INIT_THREAD_INFO(init_task),
	.stack_refcount	= ATOMIC_INIT(1),
#endif
	.state		= 0,
	.stack		= init_stack,
	.usage		= ATOMIC_INIT(2),
	.flags		= PF_KTHREAD,
	.prio		= MAX_PRIO - 20,
	.static_prio	= MAX_PRIO - 20,
	.normal_prio	= MAX_PRIO - 20,
  	...
}

而init_thread_union的地址在链接脚本中制定如下:

#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 = .;

也就是从init_task + THREAD_SIZE作为在el0发生异常陷入el1时使用sp_el0(thread_info顶部)作为栈指针。

(2)将vectors异常向量表入口设置到vbar_el1,关于异常向量表相关只是可参考ARM64的启动过程之(六):异常向量表的设定。

(3)这里需要说明一下:

x0 - x30 64bit 通用寄存器,只用低32bit则是 w0 - w30
FP (X29) 64bit 栈底指针
LR (X30) 64bit x30通常称为程序链接寄存器,保存跳转返回信息地址
XZR 64bit ZERO寄存器,写入此寄存器的数据被忽略,读出数据全为0 (WZR为 32bit形式)
    

即将0和lr地址依次写入sp - 16地址处,并sp = sp - 16。并将当前sp设置为FP(x29),这里主要是为了后面调用kasan_early_init和kaslr_early_init而使用,对于进入start_kernel不需要这样设置。

(4)将x21(fdt首地址)物理地址写入__fdt_pointer变量中,x5为一个临时变量用于暂存4k页中__fdt_pointer的偏移地址。

(5)将虚拟地址物理地址偏移写入变量kimage_voffset中,供后续代码使用。

(6)最后就是清零bss段,这里使用了__pi_memset函数,这个函数是一个宏定义函数,具体实现在(arch/arm64/lib/memset.S)中,为了保证函数地址无关,即在激活虚拟映射之前可以安全的调用不会出现问题而使用,这里已经开启了mmu但还是使用了__pi_memset函数存疑。

(7)这里再次将之前设置的FP,SP清空恢复到了thread_info顶部,并调用 b start_kernel彻底离开汇编,进入c代码世界。

arm64启动过程分析(七)其他一些功能实现

至此,arm64主cpu的主启动流程分析基本完毕,但在head.S中还涉及到了一些其他函数没有调用或设置没有用上,比如UEFI相关配置,关于EFI相关内容可以参考ARM64的启动过程之(五):UEFI。

kaslr,内核地址空间布局随机化相关内容没有分析,这里没有深入了解过也是跳过分析。

kasan,动态检测内存错误,与全局变量,栈,堆分配越界检测相关,没有深入了解也是跳过分析。

kvm,虚拟化拓展相关的内容基本实在el2_setup中完成,并在后续会调用kvm相关的汇编初始化,这里也是直接略过。

second cpu boot,当在start_kernel合适时机回去boot其他从cpu,而从cpu的入口在head.S中的secondary_holding_pen和secondary_entry进入的,代码如下:

	/*
	 * This provides a "holding pen" for platforms to hold all secondary
	 * cores are held until we're ready for them to initialise.
	 */
ENTRY(secondary_holding_pen)
	bl	el2_setup			// Drop to EL1, w0=cpu_boot_mode
	bl	set_cpu_boot_mode_flag
	mrs	x0, mpidr_el1
	mov_q	x1, MPIDR_HWID_BITMASK
	and	x0, x0, x1
	adr_l	x3, secondary_holding_pen_release
pen:	ldr	x4, [x3]
	cmp	x4, x0
	b.eq	secondary_startup
	wfe
	b	pen
ENDPROC(secondary_holding_pen)
    
	/*
	 * Secondary entry point that jumps straight into the kernel. Only to
	 * be used where CPUs are brought online dynamically by the kernel.
	 */
ENTRY(secondary_entry)
	bl	el2_setup			// Drop to EL1
	bl	set_cpu_boot_mode_flag
	b	secondary_startup
ENDPROC(secondary_entry)

secondary_startup:
	/*
	 * Common entry point for secondary CPUs.
	 */
	bl	__cpu_secondary_check52bitva
	bl	__cpu_setup			// initialise processor
	adrp	x1, swapper_pg_dir
	bl	__enable_mmu
	ldr	x8, =__seconyidary_switched
	br	x8
ENDPROC(secondary_startup)

secondary_holding_pen主要用于spin_table方式的从cpu入口点,secondary_entry是psci方式从el3 boot的入口点。但基本逻辑一致并且从cpu的初始化和主cpu流程差距不大,这里也是直接跳过分析。

最后还有armv8 拓展功能相关sve,Statistical profiling,LORegions,Debug,Performance Monitors这些都是提了一下略过细节。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

ARM64启动过程分析 的相关文章

随机推荐