Linux:ARM64 启动流程

文章目录

  • [1. 前言](#1. 前言)
  • [2. ARM64 Linux 启动流程概要](#2. ARM64 Linux 启动流程概要)

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. ARM64 Linux 启动流程概要

  • 进入内核汇编入口,切入 EL1
c 复制代码
// arch/arm64/kernel/head.S

	__HEAD
_head:
	/*
	 * DO NOT MODIFY. Image header expected by Linux boot-loaders.
	 */
#ifdef CONFIG_EFI
	/*
	 * This add instruction has no meaningful effect except that
	 * its opcode forms the magic "MZ" signature required by UEFI.
	 */
	add	x13, x18, #0x16
	b	stext
#else
	b	stext				// branch to kernel start, magic
	.long	0				// reserved
#endif

__INIT

	/*
	 * 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 // 保存 bootloader 从 x0 .. x3 传递的参数到 boot_args[]
	bl	el2_setup			// Drop to EL1, w0=cpu_boot_mode (进入 EL1)
	adrp	x23, __PHYS_OFFSET // x23 = 内核镜像区起始物理地址
	and	x23, x23, MIN_KIMG_ALIGN - 1	// KASLR offset, defaults to 0
	bl	set_cpu_boot_mode_flag	
	// 创建初始页表:
	// - 通过 idmap_pg_dir[] 页表 建立 idmap 区间的 idmap 映射
	// - 通过 swapper_pg_dir[] 页表 建立 内核镜像区 映射
	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.
	 */
	// 设定 TTBR0 和 TTBR1 各自寻址的虚拟地址区间范围 等
	bl	__cpu_setup			// initialise processor
	b	__primary_switch
ENDPROC(stext)
  • 创建初始页表

__create_page_tables 建立内核运行的初始页表,映射内核镜像区,主要是让 MMU 开启后内核可以正常运行:

c 复制代码
// 创建初始页表:
// - 通过 idmap_pg_dir[] 页表 建立 idmap 区间的 idmap 映射
// - 通过 swapper_pg_dir[] 页表 建立 内核镜像区 映射
__create_page_tables:
	mov	x28, lr

	/*
	 * Invalidate the idmap and swapper page tables to avoid potential
	 * dirty cache lines being evicted.
	 */
	adrp	x0, idmap_pg_dir
	ldr	x1, =(IDMAP_DIR_SIZE + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE)
	bl	__inval_dcache_area

	/*
	 * Clear the idmap and swapper page tables.
	 */
	// 将 idmap 和 swapper 页表清 0
	adrp	x0, idmap_pg_dir // x0 = idmap_pg_dir[] 物理地址
	ldr	x1, =(IDMAP_DIR_SIZE + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE)
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
	b.ne	1b

	mov	x7, SWAPPER_MM_MMUFLAGS

	/*
	 * Create the identity mapping.
	 */
	// x0 = idmap_pg_dir[] 的物理地址
	adrp	x0, idmap_pg_dir
	// x3 = idmap 代码区 起始位置 物理地址
	adrp	x3, __idmap_text_start		// __pa(__idmap_text_start)

	...

	// 建立   (__idmap_text_start, __idmap_text_end] 区 
	// (在 idmap_pg_dir[] 页表中的) 的 idmap 映射:
	//                Input Adress(IA)                                Output Adress(OA)
	// [pa(__idmap_text_start), pa(__idmap_text_end)] => [pa(__idmap_text_start), pa(__idmap_text_end)]
	create_pgd_entry x0, x3, x5, x6
	mov	x5, x3				// __pa(__idmap_text_start)
	adr_l	x6, __idmap_text_end		// __pa(__idmap_text_end)
	create_block_map x0, x7, x3, x5, x6

	/*
	 * Map the kernel image (starting with PHYS_OFFSET).
	 */
	// 建立 内核镜像 (在 swapper_pg_dir[] 页表中的) 的 映射
	adrp	x0, swapper_pg_dir
	mov_q	x5, KIMAGE_VADDR + TEXT_OFFSET	// compile time __va(_text)
	add	x5, x5, x23			// add KASLR displacement
	// 建立 内核镜像 首个 page 在页表 swapper_pg_dir[] 中的 pgd, (pud, ) pte 各级别页表项
	create_pgd_entry x0, x5, x3, x6
	// 建立 内核镜像 剩余 page 在页表 swapper_pg_dir[] 中的 pte 级别剩余页表项.
	// 内核镜像 在 pgd + (pud, ) 页表级别 各 只需要一个 页表项, 剩余绪建立的页
	// 表项都只在 pte 级别. 
	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)
	create_block_map x0, x7, x3, x5, x6

	/*
	 * 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
	ldr	x1, =(IDMAP_DIR_SIZE + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE)
	dmb	sy
	bl	__inval_dcache_area

	ret	x28
ENDPROC(__create_page_tables)
	.ltorg
  • CPU 初始化

主要是为开启 MMU 做准备,如清除 TLB,设置内存区域属性,TTBR1, TTBR1 各自的 VA 寻址区间(也间接划定了内核和用户空间各自的 VA 范围:TTBR1 寻址内核 VA,TTBR0 寻址用户空间 VA)等工作:

c 复制代码
// arch/arm64/mm/proc.S

.pushsection ".idmap.text", "awx"
ENTRY(__cpu_setup)
	tlbi	vmalle1				// Invalidate local TLB
	dsb	nsh

	mov	x0, #3 << 20
	msr	cpacr_el1, x0			// Enable FP/ASIMD
	mov	x0, #1 << 12			// Reset mdscr_el1 and disable
	msr	mdscr_el1, x0			// access to the DCC from EL0
	isb					// Unmask debug exceptions now,
	enable_dbg				// since this is per-cpu
	reset_pmuserenr_el0 x0			// Disable PMU access from EL0
	/*
	 * 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
	 */
	// LPAE 内存属性配置:
	// DDI0487A_j_armv8_arm.pdf, P2049
	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)
	msr	mair_el1, x5
	/*
	 * Prepare SCTLR
	 */
	// DDI0487A_j_armv8_arm.pdf, P2086
	// 设置 crval 到 sctlr_el1
	adr	x5, crval
	ldp	w5, w6, [x5]
	mrs	x0, sctlr_el1 // x0 = sctlr_el1
	bic	x0, x0, x5			// clear bits
	orr	x0, x0, x6			// set bits
	/*
	 * Set/prepare TCR and TTBR. We use 512GB (39-bit) address range for
	 * both user and kernel.
	 */
	// TCR_EL1:
	// - TCR_TxSZ(VA_BITS = 48): 设定 TTBR0 和 TTBR1 寻址区间
	//                           TTBR0: [0x0000_0000_0000_0000, 0x0000_FFFF_FFFF_FFFF]
	//                           TTBR1: [0xFFFF_0000_0000_0000, 0xFFFF_FFFF_FFFF_FFFF]
	// - TCR_A1: TTBR1_EL1.ASID 定义 ASID
	// - 内核 + 用户 4KB page
	// - 其它
	ldr	x10, =TCR_TxSZ(VA_BITS) | TCR_CACHE_FLAGS | TCR_SMP_FLAGS | \
			TCR_TG_FLAGS | TCR_ASID16 | TCR_TBI0 | TCR_A1
	// arch/arm64/include/asm/assembler.h
	tcr_set_idmap_t0sz	x10, x9 // VA_48 下为空操作

	/*
	 * Read the PARange bits from ID_AA64MMFR0_EL1 and set the IPS bits in
	 * TCR_EL1.
	 */
	// DDI0487A_j_armv8_arm.pdf, P2022
	mrs	x9, ID_AA64MMFR0_EL1
	bfi	x10, x9, #32, #3 // ??? x10[34:32] = x9[34:32]
#ifdef CONFIG_ARM64_HW_AFDBM
	/*
	 * Hardware update of the Access and Dirty bits.
	 */
	mrs	x9, ID_AA64MMFR1_EL1
	and	x9, x9, #0xf
	cbz	x9, 2f
	cmp	x9, #2
	b.lt	1f
#ifdef CONFIG_ARM64_ERRATUM_1024718
	/* Disable hardware DBM on Cortex-A55 r0p0, r0p1 & r1p0 */
	cpu_midr_match MIDR_CORTEX_A55, MIDR_CPU_VAR_REV(0, 0), MIDR_CPU_VAR_REV(1, 0), x1, x2, x3, x4
	cbnz	x1, 1f
#endif
	orr	x10, x10, #TCR_HD		// hardware Dirty flag update
1:	orr	x10, x10, #TCR_HA		// hardware Access flag update
2:
#endif	/* CONFIG_ARM64_HW_AFDBM */
	// DDI0487A_j_armv8_arm.pdf, P2101
	// 设定:
	// - 某范围的 VA 使用哪个 TTBR 来进行页表遍历
	// - 页表项格式
	// - shareability 和 cacheability
	msr	tcr_el1, x10
	ret					// return to head.S
ENDPROC(__cpu_setup)
  • 启用 MMU,进入虚拟地址的世界

在语句 b __primary_switch 之前,MMU 处于关闭状态,使用的物理寻址,现在要开启 MMU,开始使用页表进行寻址了:

c 复制代码
__primary_switch:
#ifdef CONFIG_RANDOMIZE_BASE
	mov	x19, x0				// preserve new SCTLR_EL1 value
	mrs	x20, sctlr_el1			// preserve old SCTLR_EL1 value
#endif

	bl	__enable_mmu // 启用 mmu, 期间会设置 TTBR0 & TTBR1
	...
	ldr	x8, =__primary_switched
	adrp	x0, __PHYS_OFFSET
	br	x8 // 跳转到 __primary_switched 处执行
ENDPROC(__primary_switch)
  • 进入内核 C 入口

做一些进入内核 C 入口的准备工作,然后跳转到 start_kernel()

c 复制代码
__primary_switched: // BOOT CPU
	// 设置 首进程 EL0 栈空间
	adrp	x4, init_thread_union
	add	sp, x4, #THREAD_SIZE
	adr_l	x5, init_task
	msr	sp_el0, x5			// Save thread_info

	// 配置 EL1 异常向量表 (虚拟地址 到 vbar_el1 寄存器)
	adr_l	x8, vectors			// load VBAR_EL1 with virtual
	msr	vbar_el1, x8			// vector table address
	isb

	// 设置 首进程 EL1 栈空间
	stp	xzr, x30, [sp, #-16]! // sp[-16] = 0, sp[-8] = x30(lr), sp -= 16 ???
	mov	x29, sp // x29 = init_task 内核栈顶指针 - 16

	// __fdt_pointer = FDT 物理地址
	str_l	x21, __fdt_pointer, x5		// Save FDT pointer

	// ENTRY(kimage_vaddr)
	//     .quad		_text - TEXT_OFFSET
	//
	// x4 = kimage_vaddr 虚拟地址 
	//      (x4 = 内核镜像 起始位置 虚拟地址 _text (KERNEL_START) - TEXT_OFFSET)
	// x4 -= x0
	//      (x0 = __PHYS_OFFSET, 
	//       即 内核镜像 起始位置 虚拟地址 _text (KERNEL_START) - TEXT_OFFSET 的
	//       物理地址, 所以
	//       x4 = 内核虚拟地址 - 内核物理地址
	// kimage_voffset = x4
	//
	// ldr_l 和 str_l 定义在 arch/arm64/include/asm/assembler.h 中.
	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

	// Clear BSS
	adr_l	x0, __bss_start
	mov	x1, xzr
	adr_l	x2, __bss_stop
	sub	x2, x2, x0
	bl	__pi_memset
	dsb	ishst				// Make zero page visible to PTW

	#ifdef CONFIG_KASAN
	// 将 kasan 虚拟地址区间 映射到 同一物理页面 kasan_zero_page[]
	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
  • 剩余的工作

进入 start_kernel() 后,剩下的主要工作是:

  • 进一步建立完整物理内存管理系统
  • 启动非 BOOT CPU

内存管理系统的建立可以参考系列文章:

Linux 内存管理 (2):memblock 子系统的建立
Linux 内存管理 (3):fixmap
Linux 内存管理 (4):buddy 管理系统的建立
Linux 内存管理 (6):slub 分配器

启动非 BOOT CPU可参考:
Linux:多核 CPU 启动流程简析

相关推荐
TroubleMakerQi3 小时前
[虚拟机环境配置]07_Ubuntu中安装vscode教程
linux·人工智能·vscode·ubuntu
源远流长jerry3 小时前
RDMA vs 传统以太网:寻址粒度为何决定性能天花板
linux·网络
zzzsde3 小时前
【Linux】进程控制(1):进程创建&&进程终止
linux·运维·服务器
顺顺 尼3 小时前
linux第一个系统程序-进度条
linux
开源盛世!!4 小时前
3.19-3.21
linux·服务器·前端
小民AI实战笔记4 小时前
htop安装不了怎么解决
linux·运维
源远流长jerry4 小时前
RDMA 技术深度解析:从原理到实践
linux·网络·tcp/ip·架构·ip
ken22324 小时前
在ubuntu终端里, 怎样让历史不要记录本条命令:禁止记录包含密码之类的命令
linux·运维·ubuntu
Lucis__4 小时前
Linux进程间通信IPC:从管道到共享内存的发展演进
linux·运维·服务器