文章目录
- [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 启动流程简析