1. 內核啟動要求
arch/arm64/boot/head.S
開始的注釋簡單說明了內核啟動的條件。更詳細的內容可以查看內核文檔Documentation/arm64/booting.rst
或Documentation/translations/zh_CN/arm64/booting.txt
。
/*
* Kernel startup entry point.
* ---------------------------
*
* The requirements are:
* MMU = off, D-cache = off, I-cache = on or off,
* x0 = physical address to the FDT blob.
*
* This code is mostly position independent so you call this at
* __pa(PAGE_OFFSET).
*
* Note that the callee-saved registers are used for storing variables
* that are useful before the MMU is enabled. The allocations are described
* in the entry routines.
*/
2. 內核啟動入口
根據Makefile對KBUILD_LDS的定義,鏈接vmlinux使用的連接腳本為arch/$(SRCARCH)/kernel/vmlinux.lds
。從鏈接腳本arch/arm64/kernel/vmlinux.lds
可以查到,程序的入口為_text
,鏡像起始位置存放的是.head.text
段生成的指令。搜索.head.text
,可以找到include/linux/init.h
對__HEAD
定義.section ".head.text","ax"
。
OUTPUT_ARCH(aarch64)
ENTRY(_text)
SECTIONS
{
. = ((((((-(((1)) << ((((48))) - 1)))) + (0x08000000))) + (0x08000000)));
.head.text : {
_text = .;
KEEP(*(.head.text))
}
...
}
/* For assembly routines */
#define __HEAD .section ".head.text","ax"
#define __INIT .section ".init.text","ax"
#define __FINIT .previous
再看一下arch/arm64/kernel/vmlinux.lds
是怎么生成的,編譯日志中,會有LDS arch/arm64/kernel/vmlinux.lds
,scripts/Makefile.build
中可以看到是對arch/arm64/kernel/vmlinux.lds.S
進行預處理得到了最終的鏈接腳本。
# Linker scripts preprocessor (.lds.S -> .lds)
# ---------------------------------------------------------------------------
quiet_cmd_cpp_lds_S = LDS $@
cmd_cpp_lds_S = $(CPP) $(cpp_flags) -P -U$(ARCH) \
-D__ASSEMBLY__ -DLINKER_SCRIPT -o $@ $<
$(obj)/%.lds: $(src)/%.lds.S FORCE
$(call if_changed_dep,cpp_lds_S)
再搜索__HEAD
,可以看到程序起始代碼位於arch/arm64/kernel/head.S
。
3. 概覽:從入口到start_kernel
從入口到start_kernel的主要是匯編代碼,后續的很多子系統都會依賴這部分代碼做的初始化。
+-- _text() // 內核啟動入口
\-- primary_entry()
+-- preserve_boot_args() // 保存x0~x3到boot_args[0~3]
+-- init_kernel_el() // 根據內核運行異常等級進行配置,返回啟動模式
| +-- init_el1() // 通常情況下從EL1啟動內核
| \-- init_el2() // 從EL2啟動內核,用於開啟VHE(Virtualization Host Extensions)
+-- set_cpu_boot_mode_flag() // 保存bootmode到__boot_cpu_mode[2]全局數組
+-- __create_page_tables() // 建立恆等映射idmap_pg_dir和內核鏡像映射init_pg_dir的頁表
+-- __cpu_setup() // 為開啟MMU做的CPU初始化
\-- __primary_switch()
+-- __enable_mmu() // 開啟MMU
\-- __primary_switched() // 初始化init_task棧,設置VBAR_EL1,保存FDT地址,計算kimage_voffset,清空bss段
+-- early_fdt_map()
| +-- early_fixmap_init() // 嘗試建立fixmap的頁表,可能失敗
| \-- fixmap_remap_fdt() // 如果成功建立fixmap頁表,將fdt映射到fixmap的FIX_FDT區域
+-- init_feature_override() // 根據BootLoader傳入的參數,對一些參數的改寫
+-- switch_to_vhe() // 需要的話,開啟VHE
+-- start_kernel() // 跳轉到start_kernel執行
4. MMU開啟之前:primary_entry
在內核啟動入口直接跳轉到primary_entry,這是MMU開啟之前所有函數的總流程。
/*
* The following callee saved general purpose registers are used on the
* primary lowlevel boot path:
*
* Register Scope Purpose
* x21 primary_entry() .. start_kernel() FDT pointer passed at boot in x0
* x23 primary_entry() .. start_kernel() physical misalignment/KASLR offset
* x28 __create_page_tables() callee preserved temp register
* x19/x20 __primary_switch() callee preserved temp registers
* x24 __primary_switch() .. relocate_kernel() current RELR displacement
*/
SYM_CODE_START(primary_entry)
bl preserve_boot_args
bl init_kernel_el // w0=cpu_boot_mode
adrp x23, __PHYS_OFFSET // 讀取內核鏡像入口`_text`的物理地址
and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0
bl set_cpu_boot_mode_flag // 保存bootmode到__boot_cpu_mode[2]
bl __create_page_tables // 建立恆等映射idmap_pg_dir和內核鏡像映射init_pg_dir的頁表
/*
* 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 // 為開啟MMU做的CPU初始化
b __primary_switch // 主要工作就是開啟MMU,之后跳轉到__primary_switched
SYM_CODE_END(primary_entry)
4.1. preserve_boot_args
主要工作是將FDT的基地址保存到x21寄存器,將啟動參數(x0~x3)保存到boot_args數組,並使用。
/*
* The recorded values of x0 .. x3 upon kernel entry.
*/
u64 __cacheline_aligned boot_args[4];
/*
* Preserve the arguments passed by the bootloader in x0 .. x3
*/
SYM_CODE_START_LOCAL(preserve_boot_args)
mov x21, x0 // x21=FDT,x0是uboot傳入的第一個參數,記錄fdt的基地址,將x0的值保存到x21寄存器備份
adr_l x0, boot_args // 讀取boot_args變量的當前地址到x0,此時MMU處於關閉狀態,訪問的是物理地址
stp x21, x1, [x0] // record the contents of x0 .. x3 at kernel entry
stp x2, x3, [x0, #16] // 將x0~x3保存到boot_args[0~3]
dmb sy // needed before dc ivac with MMU off
// 保證stp指令完成
add x1, x0, #0x20 // 4 x 8 bytes,boot_args數組的大小
b dcache_inval_poc // 使boot_args[]數組對應的高速緩存失效
SYM_CODE_END(preserve_boot_args)
dmb sy
在全系統高速緩沖范圍內做一次內存屏障,保證前面的stp指令運行順序正確,保證stp在調用dcache_inval_poc前完成。
dcache_inval_poc
傳入參數為boot_args數組的起始和結束地址,函數的作用是使boot_args數組對應的高速緩存失效,並清除這些緩存。
/*
* dcache_inval_poc(start, end)
*
* Ensure that any D-cache lines for the interval [start, end)
* are invalidated. Any partial lines at the ends of the interval are
* also cleaned to PoC to prevent data loss.
*
* - start - kernel start address of region
* - end - kernel end address of region
*/
4.2. init_kernel_el
判斷啟動的模式是EL2還是非安全模式的EL1,並進行相關級別的系統配置(ARMv8中EL2是hypervisor模式,EL1是標准的內核模式),然后使用w0返回啟動模式(BOOT_CPU_MODE_EL1或BOOT_CPU_MODE_EL2)。
#define BOOT_CPU_MODE_EL1 (0xe11)
#define BOOT_CPU_MODE_EL2 (0xe12)
/* Current Exception Level values, as contained in CurrentEL */
#define CurrentEL_EL1 (1 << 2)
#define CurrentEL_EL2 (2 << 2)
/*
* Starting from EL2 or EL1, configure the CPU to execute at the highest
* reachable EL supported by the kernel in a chosen default state. If dropping
* from EL2 to EL1, configure EL2 before configuring EL1.
*
* Since we cannot always rely on ERET synchronizing writes to sysregs (e.g. if
* SCTLR_ELx.EOS is clear), we place an ISB prior to ERET.
*
* Returns either BOOT_CPU_MODE_EL1 or BOOT_CPU_MODE_EL2 in w0 if
* booted in EL1 or EL2 respectively.
*/
SYM_FUNC_START(init_kernel_el)
mrs x0, CurrentEL // 讀取當前EL等級
cmp x0, #CurrentEL_EL2
b.eq init_el2 // 如果是EL2則跳轉到init_el2,否則繼續向下執行init_el1的代碼
SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)
...
eret
SYM_INNER_LABEL(init_el2, SYM_L_LOCAL)
...
ret
SYM_FUNC_END(init_kernel_el)
4.2.1. init_el1
配置CPU的大小端模式,將啟動模式BOOT_CPU_MODE_EL1寫入w0,然后返回到primary_entry。
#define INIT_SCTLR_EL1_MMU_OFF \
(ENDIAN_SET_EL1 | SCTLR_EL1_RES1)
SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)
mov_q x0, INIT_SCTLR_EL1_MMU_OFF // MMU關閉時,對sctlr_el1的賦值
msr sctlr_el1, x0 // 配置CPU的大小端模式,EE域用來配置EL1,E0E域用來配置EL0
isb // 配置CPU大小端模式后,確保前面的指令都運行完成。
mov_q x0, INIT_PSTATE_EL1
msr spsr_el1, x0 // 將INIT_PSTATE_EL1寫入spsr_el1
msr elr_el1, lr // 將返回地址寫入elr_el1,lr是primary_entry中`bl init_kernel_el`的下一條指令地址。
mov w0, #BOOT_CPU_MODE_EL1 // 記錄啟動模式
eret // 通過eret來使用ELR_ELx和SPSR_ELx來恢復PC和PSTATE
4.3. set_cpu_boot_mode_flag
將啟動模式保存到__boot_cpu_mode[2]全局數組。
/*
* 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.
*/
SYM_FUNC_START_LOCAL(set_cpu_boot_mode_flag)
adr_l x1, __boot_cpu_mode // x1記錄__boot_cpu_mode[]的地址
cmp w0, #BOOT_CPU_MODE_EL2 // w0記錄啟動時的異常等級
b.ne 1f // 如果不是從EL2啟動,則跳轉到1處
add x1, x1, #4 // 如果是從EL2啟動,地址指向__boot_cpu_mode[1]
1: str w0, [x1] // Save CPU boot mode,保存啟動模式到x1指向的地址,如果是從EL1啟動,地址指向__boot_cpu_mode[0]
dmb sy // 保證str指令執行完成
dc ivac, x1 // Invalidate potentially stale cache line,使高速緩存失效
ret
SYM_FUNC_END(set_cpu_boot_mode_flag)
if (w0 == BOOT_CPU_MODE_EL2) {
__boot_cpu_mode[1] = BOOT_CPU_MODE_EL2;
} else {
__boot_cpu_mode[0] = BOOT_CPU_MODE_EL1;
}
4.4. __create_page_tables
創建內存映射,一共兩張,一張存放在idmap_pg_dir(恆等映射,物理地址和虛擬地址相同),一張存放在init_pg_dir(線性映射)。
4.5. __cpu_setup
為開啟MMU而初始化處理器相關的代碼,配置MMU,配置訪問權限,內存地址划分等。
函數返回是x0記錄了SCTLR_EL1要寫入的值,最后傳給__enable_mmu。
5. 開啟MMU:__primary_switch
__primary_switch
表示重要的切換,這個非常重要的切換就是開啟MMU。開啟MMU(__enable_mmu)之前,CPU使用物理地址訪問內存,自__primary_switched
開始,CPU會以虛擬地址來訪問內存。
SYM_FUNC_START_LOCAL(__primary_switch)
#ifdef CONFIG_RANDOMIZE_BASE
...
#endif
/*
* x0 = SCTLR_EL1 value for turning on the MMU.
* x1 = TTBR1_EL1 value
*/
adrp x1, init_pg_dir
bl __enable_mmu
#ifdef CONFIG_RELOCATABLE
...
#endif
ldr x8, =__primary_switched // __primary_switched的鏈接地址
adrp x0, __PHYS_OFFSET // 內核鏡像起始的物理地址
br x8 // 跳轉到__primary_switched虛擬地址運行
SYM_FUNC_END(__primary_switch)
5.1. __enable_mmu
主要工作:
- 檢查CPU是否支持軟件設置的頁面大小,如果不支持,CPU會在停止在這里。
- 將idmap_pg_dir和init_pg_dir分別加載到TTBR0_EL1和TTBR1_EL1。
- 開啟MMU,並使本地icache失效。
6. 開啟MMU之后:__primary_switched
開啟MMU之后,CPU訪問的是虛擬地址。
- 准備0號進程內核棧
- 中斷向量表配置
- 計算kimage_voffset
- 清空BSS段
- 嘗試fixmap映射
- 在
__primary_switch
的最后,跳轉到虛擬地址之前,使用adrp x0, __PHYS_OFFSET
記錄了_text
的地址,也就是內核鏡像起始的物理地址。
#define __PHYS_OFFSET KERNEL_START
#define KERNEL_START _text
/*
* The following fragment of code is executed with the MMU enabled.
*
* x0 = __PHYS_OFFSET
*/
SYM_FUNC_START_LOCAL(__primary_switched)
6.1. 初始化init_task棧空間
主要工作:
- 設置SP_EL0、SP_ELx、x29(FP)寄存器,配置init_task的棧
- 將per_cpu_offset寫入TPIDR_ELx
adr_l x4, init_task
init_cpu_task x4, x5, x6
6.1.1. init_cpu_task
先看一下涉及到的幾個宏。
#define TSK_STACK 24 /* offsetof(struct task_struct, stack) */
#define S_STACKFRAME 304 /* offsetof(struct pt_regs, stackframe) */
#define PT_REGS_SIZE 336 /* sizeof(struct pt_regs) */
#define TSK_CPU 64 /* offsetof(struct task_struct, cpu) */
在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,內核空間中會使用sp_el0來作為current
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
6.2. 中斷向量表配置
將中斷向量表的起始虛擬地址寫入到VBAR_EL1。
adr_l x8, vectors // load VBAR_EL1 with virtual
msr vbar_el1, x8 // vector table address
isb
6.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函數調用標准規定的棧布局,為后續函數調用的入棧出棧做好了准備。
stp x29, x30, [sp, #-16]!
mov x29, sp
6.4. 保存設備樹物理地址到__fdt_pointer
str_l x21, __fdt_pointer, x5 // Save FDT pointer
6.5. 計算kimage_voffset
kimage_voffset
記錄了內核鏡像映射后的虛擬地址與內核鏡像在內存中的物理地址之間的差值。kimage_vaddr
記錄了_text
的鏈接地址,也就是最終_text
的虛擬地址,x0作為傳入參數記錄了_text
的物理地址,相減即可得kimage_voffset
。
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
6.6. 清空BSS段
// Clear BSS
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
6.7. early_fdt_map
early_fdt_map主要為KASLR服務,可能會失敗,如果失敗,會在setup_arch重新映射。
#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
bl init_feature_override // Parse cpu feature overrides
#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
6.8. 跳轉start_kernel
從棧中恢復x29(FP)和x30(LR),sp重新指向init_task.stack + THREAD_SIZE - sizeof(struct pt_regs)
。
ldp x29, x30, [sp], #16 // 從棧中恢復x29(FP)和x30(LR),sp += 16
bl start_kernel
ASM_BUG() // start_kernel返回到這里說明出錯了
SYM_FUNC_END(__primary_switched)
7. 參考資料
Documentation/arm64/booting.rst
Documentation/translations/zh_CN/arm64/booting.txt
- Linux 內核啟動分析-BugMan-ChinaUnix博客
- 中斷管理基礎學習筆記 - 5.1 ARM64底層中斷處理
- Linux kernel ARM64 寄存器tpidr_el1 的用處
- arm64: Extract early FDT mapping from kaslr_early_init()