linux内核启动流程分析1

  • *简单分析启动过程中的初始化内容

1. 寻找执行入口

  • 和之前找uboot的执行入口一样,在编译时查看编译文件的顺序和链接文件发现入口为arch/armc/kernel/head.S

“arch/arm/kernel/vmlinux.lds”

. = 0xC0000000 + 0x00008000
.head.text : {
text = .;
*(.head.text)
}

“arch/arm/kernel/head.S”

.arm
__HEAD
ENTRY(stext)

“include/linux/init.h”

#define __HEAD .section “.head.text”, “ax”
#define __INIT .section “.init.text”, “ax”
#define __FINIT .previous

2. head.S

  1. 确定 CPU 类型和启动模式(ARM/Thumb、SVC 模式)

  2. 关闭中断

  3. 检查处理器是否支持必要功能

  4. 初始化物理地址偏移

  5. 验证 ATAGs/DTB 参数

  6. 创建初始页表(identity mapping)

  7. 初始化 CPU 特定功能

  8. 开启 MMU(启用虚拟地址空间)

  9. 跳转到 __mmap_switched 进入内核下一阶段

  10. 段和入口定义

1
2
3
.arm
__HEAD // 定义在 .head.text 段
ENTRY(stext) // 内核入口符号 stext
  • .head.text 段:用于放置启动代码。
  • stext 是 ARM Linux 的 第一个执行入口(在物理地址 0xC0008000,即 PAGE_OFFSET + TEXT_OFFSET)。
  1. 检查 ARM/Thumb 模式并切换
1
2
3
4
THUMB( adr r9, BSYM(1f) )
THUMB( bx r9 )
THUMB( .thumb )
THUMB(1: )
  • 如果内核是 Thumb-2 格式(一种ARM的嵌入式格式),先跳转并切换到 Thumb 状态。
  • 否则继续 ARM 模式。
  1. 设置 CPU 为 SVC 模式,关闭 IRQ/FIQ
1
setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9
  • PSR_F_BIT:屏蔽 FIQ
  • PSR_I_BIT:屏蔽 IRQ
  • SVC_MODE:进入管理模式(Supervisor mode)

保证启动过程不会被中断打断。

  1. 获取 CPU ID 并匹配处理器类型
1
2
3
4
mrc p15, 0, r9, c0, c0     @ 获取 CP15 c0:主 ID 寄存器
bl __lookup_processor_type @ 查找 CPU 类型表,返回 r5=procinfo
movs r10, r5 @ 如果 r5=0,说明不支持该 CPU
beq __error_p @ 出错处理
  • 读取 CPU ID(ARM 架构寄存器)。
  • 检查是否在 内核支持的 CPU 列表里。
  1. LPAE(ARMv7+)检查
1
2
3
4
mrc p15, 0, r3, c0, c1, 4   @ 读取 ID_MMFR0
and r3, r3, #0xf
cmp r3, #5 @ 检查是否支持长描述符页表
blo __error_p
  • 如果是 ARM LPAE 模式,检查是否支持 64-bit 页表。
  1. 计算 PHYS_OFFSET
1
2
3
4
adr r3, 2f
ldmia r3, {r4, r8}
sub r4, r3, r4
add r8, r8, r4
  • 通过位置无关代码计算 物理偏移量,用于页表映射。
  • 如果 CONFIG_XIP_KERNEL(执行在闪存),则直接 ldr r8, =PHYS_OFFSET。
  1. 验证传入参数
1
2
3
4
5
6
7
bl __vet_atags       @ 检查 ATAGs 或 Device Tree
#ifdef CONFIG_SMP_ON_UP
bl __fixup_smp @ 单核系统伪 SMP 修复
#endif
#ifdef CONFIG_ARM_PATCH_PHYS_VIRT
bl __fixup_pv_table @ 修正物理/虚拟基址差异
#endif
  • 检查 启动参数有效性(machine ID, atags/dtb)。
  • 如果是单核系统 + SMP 配置,做补丁。
  • 修补物理/虚拟地址差异。
  1. 创建启动页表
1
bl __create_page_tables
  • 这是关键步骤,创建初始页表,映射:
    • identity mapping:物理地址映射到相同的虚拟地址(用于开启 MMU)
    • 内核虚拟基地址 (0xC0000000) 映射到物理地址
    • UART IO 区域(如果开启 DEBUG_LL)
  • 进入 __create_page_tables 的逻辑:
    • 分配页表内存 (pgtbl)
    • 清空页表
    • identity map 启动代码区域(__turn_mmu_on)
    • 映射内核代码区域 [KERNEL_START, KERNEL_END)
    • 映射 boot 参数区(ATAGs 或 DTB)
    • 如果 DEBUG_LL:映射 UART 寄存器地址
    • 返回页表基地址(r4)
  1. 初始化 CPU
1
2
3
4
ldr r13, =__mmap_switched   @ MMU 打开后跳转地址
adr lr, BSYM(1f) @ 返回地址
mov r8, r4 @ 设置页表基地址
ARM( add pc, r10, #PROCINFO_INITFUNC )
  • 调用 CPU 特定初始化代码(在 arch/arm/mm/proc-*.S)。
  • 初始化完成后返回,准备打开 MMU。
  1. 打开 MMU
1
b __enable_mmu
  • __enable_mmu:
    • 配置 域访问控制寄存器(domain access control)
    • 设置页表基地址(TTBR)
    • 修改 CP15 控制寄存器(r0),打开 MMU(bit 0),可能还打开缓存。

然后执行:

1
b __turn_mmu_on
  • __turn_mmu_on:
    • 写 CP15 控制寄存器,开启 MMU
    • 切换 PC 到 __mmap_switched(位于高地址 0xC0000000 区域)
    • 从此开始,虚拟地址生效。
  1. __mmap_switched(MMU 打开后的第一步)
  • 这个在 arch/arm/kernel/head-common.S,作用:
    • 它完成从低级引导环境到C 语言内核环境的最后过渡,设置栈指针到内核栈, 清空 .bss, 确保内存布局、栈和关键变量正确,然后跳转到 start_kernel()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/*
* __mmap_switched:
* - MMU 已开启后执行的第一段代码
* - 作用:完成 .data 段拷贝、.bss 清零、设置栈、保存启动参数,然后跳到 start_kernel()
*/
__mmap_switched:
adr r3, __mmap_switched_data @ r3 <- __mmap_switched_data 表虚拟地址

/* ---------------------------------------------------------
* 1. 复制 .data 段(如果物理地址 != 虚拟地址)
* 从 r3 中加载 4 个地址:__data_loc, _sdata, __bss_start, _end
* --------------------------------------------------------- */
ldmia r3!, {r4, r5, r6, r7} @ r4=__data_loc, r5=_sdata, r6=__bss_start, r7=_end
cmp r4, r5 @ 比较 __data_loc 和 _sdata
1: cmpne r5, r6 @ 如果 .data 还没拷贝完并且不等于 .bss 开始
ldrne fp, [r4], #4 @ 从源地址(r4)加载一个字(递增4
strne fp, [r5], #4 @ 存到目标地址(r5),并递增
bne 1b @ 如果还没结束,继续循环

/* ---------------------------------------------------------
* 2. 清零 .bss 段(__bss_start 到 _end)
* --------------------------------------------------------- */
mov fp, #0 @ fp 清零,用于写入
1: cmp r6, r7 @ 比较当前位置和 _end
strcc fp, [r6],#4 @ 如果 r6 < r7,则写 0 并递增
bcc 1b @ 继续循环直到 r6 >= r7

/* ---------------------------------------------------------
* 3. 加载全局变量地址 + 设置栈
* 从表中再加载 5 个值:processor_id, machine_arch_type, atags_pointer, cr_alignment, sp
* --------------------------------------------------------- */
ARM( ldmia r3, {r4, r5, r6, r7, sp} ) @ ARM 模式直接加载5个寄存器
THUMB( ldmia r3, {r4, r5, r6, r7} ) @ Thumb 模式先加载4
THUMB( ldr sp, [r3, #16] ) @ Thumb 再单独加载栈指针

/* ---------------------------------------------------------
* 4. 保存启动参数到全局变量
* r9=CPU ID, r1=Machine ID, r2=atags/dtb 指针
* --------------------------------------------------------- */
str r9, [r4] @ 保存 processor_id
str r1, [r5] @ 保存 machine_arch_type
str r2, [r6] @ 保存 atags/dtb pointer

/* ---------------------------------------------------------
* 5. 保存 CP15 控制寄存器值
* r0 中存的是 CP15 控制寄存器(MMU 等)
* CR_A 位是 Alignment fault enable,通常清掉
* --------------------------------------------------------- */
bic r4, r0, #CR_A @ r4 = r0 去掉 Alignment 位
stmia r7, {r0, r4} @ 保存 r0 和 r4 到 cr_alignment 区

/* ---------------------------------------------------------
* 6. 跳转到 C 语言内核入口
* --------------------------------------------------------- */
b start_kernel @ 进入 start_kernel() (init/main.c)
ENDPROC(__mmap_switched)

/*
* __mmap_switched_data 表:
* 提供初始化所需的虚拟地址(因为 MMU 已开启)
*/
__mmap_switched_data:
.long __data_loc @ r4:物理 .data 起始地址
.long _sdata @ r5:虚拟 .data 起始地址
.long __bss_start @ r6:.bss 起始地址
.long _end @ r7:内核结束地址
.long processor_id @ 保存 CPU ID 的地址
.long __machine_arch_type @ 保存机器 ID 的地址
.long __atags_pointer @ 保存 atags/dtb 指针的地址
.long cr_alignment @ 保存 CP15 控制寄存器值的地址
.long init_thread_union + THREAD_START_SP @ 栈指针地址
.size __mmap_switched_data, . - __mmap_switched_data
  1. 进入 __mmap_switched
1
2
__mmap_switched:
adr r3, __mmap_switched_data
  • adr r3, __mmap_switched_data:获取 __mmap_switched_data 表的虚拟地址(因为 MMU 已开启,现在是虚拟地址访问)。
  1. 复制 .data 段
1
2
3
4
5
6
ldmia   r3!, {r4, r5, r6, r7}   // 从表中读取4个值:__data_loc, _sdata, __bss_start, _end
cmp r4, r5 // 比较 __data_loc 和 _sdata
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b

含义:

  • r4 = __data_loc:物理内存中 .data 的位置
  • r5 = _sdata:虚拟地址 .data 段起始位置
  • r6 = __bss_start:.bss 段开始
  • r7 = _end:内核镜像结束位置

循环:如果 .data 的物理位置和虚拟位置不同(XIP 或 relocate),复制 .data 段内容到正确位置。

  1. 清零 BSS
1
2
3
4
mov fp, #0              @ 清零寄存器 fp
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
  • 把 .bss 段清零(即 __bss_start 到 _end)。
    • 原因:C 语言要求未初始化全局变量为 0。
  1. 加载保存关键变量的地址
1
2
3
ARM(   ldmia  r3, {r4, r5, r6, r7, sp})
THUMB( ldmia r3, {r4, r5, r6, r7} )
THUMB( ldr sp, [r3, #16] )
  • 现在 r3 指向 __mmap_switched_data 的剩余部分:
    • r4 = processor_id 地址
    • r5 = __machine_arch_type 地址
    • r6 = __atags_pointer 地址
    • r7 = cr_alignment 地址
    • sp = init_thread_union + THREAD_START_SP(设置栈指针)
  1. 保存启动参数
1
2
3
str r9, [r4]    @ processor_id = r9
str r1, [r5] @ machine_arch_type = r1
str r2, [r6] @ atags/dtb pointer = r2
  • 把早期保存的 CPU ID(r9)、机器类型(r1)、atags/dtb 地址(r2)存到全局变量。
  • 这样 start_kernel() 就可以用这些数据。
  1. 保存控制寄存器值
1
2
bic r4, r0, #CR_A
stmia r7, {r0, r4} @ 保存 CP15 control register

r0 是 CP15 控制寄存器值(MMU、Cache 状态)。
清掉 CR_A(Alignment fault enable),保存两个版本:
r0 原始值
r4 去掉 Alignment bit 的值

  1. 跳转到 C 语言世界
1
b start_kernel
  • 直接跳到 start_kernel()(init/main.c),进入 Linux C 初始化。

  • __mmap_switched_data

1
2
3
4
5
6
7
8
9
10
__mmap_switched_data:
.long __data_loc @ r4
.long _sdata @ r5
.long __bss_start @ r6
.long _end @ r7
.long processor_id @ 保存 CPU ID 的地址
.long __machine_arch_type @ 保存机器 ID
.long __atags_pointer @ 保存启动参数地址
.long cr_alignment @ 保存 CP15 控制寄存器值
.long init_thread_union + THREAD_START_SP @ sp
  • 这个表提供了所有初始化需要的虚拟地址,因为现在 MMU 打开了,可以直接使用虚拟地址。

总结:__mmap_switched 主要干的事
✔ 复制 .data 段(如果需要)
✔ 清零 .bss 段
✔ 设置栈指针(SP)
✔ 保存 CPU ID、Machine ID、ATAGS/DTB 地址到全局变量
✔ 保存 CP15 控制寄存器值
✔ 跳转到 start_kernel() 进入 C 世界

作者

GoKo Mell

发布于

2024-10-10

更新于

2025-09-11

许可协议

# 相关文章
  1.linux内核启动流程分析2
评论

:D 一言句子获取中...