Linux ARM64 内核解读之内核引导和初始化
处理器上电以后,首先执行引导程序,引导程序把内核加载到内存,然后执行内核,内核初始化完成以后,启动用户的第一个进程。
一、 到哪里读取引导程序
处理器到哪里读取引导程序的指令?处理器在上电时自动把程序计数器设置为处理器常识设计的某个固定值,对于ARM64处理器,这个固定值是0。处理器的内存管理单元(MMU)负责把虚拟地址转换为物理地址,ARM64处理器刚上电的时候没有开启内存管理单元,物理地址和虚拟地址相同,所以AMR64处理器到物理地址0取第一条指令。
嵌入式设备通常使用NOR闪存作为只读存储器来存放引导程序。NOR闪存的容量比较小,最小的读写单位是字节,程序可以直接在芯片内执行。从物理地址0开始的一段物理空间地址被分配和NOR闪存。
综上所述,ARM64处理器到虚拟地址0取指令,就是到物理地址0取指令,也就是到NOR闪存的起始位置取指令。
二、 引导程序
嵌入式设备通常使用U-boot作为引导程序。
下面简要介绍一下ARM64处理器的U-Boot程序的执行过程,入口是文件“arch/arm/cpu/armv8/start.S”定义的_start,我们从标号_start开始分析。
2.1 入口_start
标号_start是U-boot程序的入口,直接跳转到标号reset执行。
2.2 标号reset
从标号reset开始的代码如下:
55 reset:56 /* Allow the board to save important registers */57 b save_boot_params58 .globl save_boot_params_ret59 save_boot_params_ret:60 61 #if CONFIG_POSITION_INDEPENDENT && !defined(CONFIG_SPL_BUILD)62 /* Verify that we're 4K aligned. */63 adr x0, _start64 ands x0, x0, #0xfff65 b.eq 1f66 0:67 /*68 * FATAL, can't continue.69 * U-Boot needs to be loaded at a 4K aligned address.70 *71 * We use ADRP and ADD to load some symbol addresses during startup.72 * The ADD uses an absolute (non pc-relative) lo12 relocation73 * thus requiring 4K alignment.74 */75 wfi76 b 0b77 1:78 79 /*80 * Fix .rela.dyn relocations. This allows U-Boot to be loaded to and81 * executed at a different address than it was linked at.82 */83 pie_fixup:84 adr x0, _start /* x0 <- Runtime value of _start */85 ldr x1, _TEXT_BASE /* x1 <- Linked value of _start */86 subs x9, x0, x1 /* x9 <- Run-vs-link offset */87 beq pie_fixup_done88 adrp x2, __rel_dyn_start /* x2 <- Runtime &__rel_dyn_start */89 add x2, x2, #:lo12:__rel_dyn_start90 adrp x3, __rel_dyn_end /* x3 <- Runtime &__rel_dyn_end */91 add x3, x3, #:lo12:__rel_dyn_end92 pie_fix_loop:93 ldp x0, x1, [x2], #16 /* (x0, x1) <- (Link location, fixup) */94 ldr x4, [x2], #8 /* x4 <- addend */95 cmp w1, #1027 /* relative fixup? */96 bne pie_skip_reloc97 /* relative fix: store addend plus offset at dest location */98 add x0, x0, x999 add x4, x4, x9
100 str x4, [x0]
101 pie_skip_reloc:
102 cmp x2, x3
103 b.lo pie_fix_loop
104 pie_fixup_done:
105 #endif
106
107 #if defined(CONFIG_ARMV8_SPL_EXCEPTION_VECTORS) || !defined(CONFIG_SPL_BUILD)
108 .macro set_vbar, regname, reg
109 msr \regname, \reg
110 .endm
111 adr x0, vectors
112 #else
113 .macro set_vbar, regname, reg
114 .endm
115 #endif
116 /*
117 * Could be EL3/EL2/EL1, Initial State:
118 * Little Endian, MMU Disabled, i/dCache Disabled
119 */
120 switch_el x1, 3f, 2f, 1f
121 3: set_vbar vbar_el3, x0
122 mrs x0, scr_el3
123 orr x0, x0, #0xf /* SCR_EL3.NS|IRQ|FIQ|EA */
124 msr scr_el3, x0
125 msr cptr_el3, xzr /* Enable FP/SIMD */
126 b 0f
127 2: mrs x1, hcr_el2
128 tbnz x1, #HCR_EL2_E2H_BIT, 1f /* HCR_EL2.E2H */
129 orr x1, x1, #HCR_EL2_AMO_EL2 /* Route SErrors to EL2 */
130 msr hcr_el2, x1
131 set_vbar vbar_el2, x0
132 mov x0, #0x33ff
133 msr cptr_el2, x0 /* Enable FP/SIMD */
134 b 0f
135 1: set_vbar vbar_el1, x0
136 mov x0, #3 << 20
137 msr cpacr_el1, x0 /* Enable FP/SIMD */
138 0:
139 msr daifclr, #0x4 /* Unmask SError interrupts */
140
141 #if CONFIG_COUNTER_FREQUENCY
142 branch_if_not_highest_el x0, 4f
143 ldr x0, =CONFIG_COUNTER_FREQUENCY
144 msr cntfrq_el0, x0 /* Initialize CNTFRQ */
145 #endif
146
147 4: isb
148
149 /*
150 * Enable SMPEN bit for coherency.
151 * This register is not architectural but at the moment
152 * this bit should be set for A53/A57/A72.
153 */
154 #ifdef CONFIG_ARMV8_SET_SMPEN
155 switch_el x1, 3f, 1f, 1f
156 3:
157 mrs x0, S3_1_c15_c2_1 /* cpuectlr_el1 */
158 orr x0, x0, #0x40
159 msr S3_1_c15_c2_1, x0
160 isb
161 1:
162 #endif
163
164 /* Apply ARM core specific erratas */
165 bl apply_core_errata
166
167 /*
168 * Cache/BPB/TLB Invalidate
169 * i-cache is invalidated before enabled in icache_enable()
170 * tlb is invalidated before mmu is enabled in dcache_enable()
171 * d-cache is invalidated before enabled in dcache_enable()
172 */
173
174 /* Processor specific initialization */
175 bl lowlevel_init
176
177 #if defined(CONFIG_ARMV8_SPIN_TABLE) && !defined(CONFIG_SPL_BUILD)
178 branch_if_master x0, master_cpu
179 b spin_table_secondary_jump
180 /* never return */
181 #elif defined(CONFIG_ARMV8_MULTIENTRY)
182 branch_if_master x0, master_cpu
183
184 /*
185 * Slave CPUs
186 */
187 slave_cpu:
188 wfe
189 ldr x1, =CPU_RELEASE_ADDR
190 ldr x0, [x1]
191 cbz x0, slave_cpu
192 br x0 /* branch to the given address */
193 #endif /* CONFIG_ARMV8_MULTIENTRY */
194 master_cpu:
195 msr SPSel, #1 /* make sure we use SP_ELx */
196 bl _main
第57行代码,调用各种板卡自定义的函数save_boot_params来保存重要的寄存器。
第120-140行代码,根据处理器当前的异常级别设置寄存器。
1)第121-126行代码,如果异常级别是3,那么把向量基准寄存器(VBAR_EL3)设置为异常向量的起始地址;设置安全配置寄存器(SCR_EL3)的NS、IRQ、FIQ和EA这4个位,也就是异常级别0和1处于非安全状态,在任何异常级别执行时都把中断、快速中断、同步外部中止和系统错误转发到异常级别3;把协处理器陷入寄存器(CPTR_EL3)设置为0,允许访问浮点和单指令多数据(SIMD)功能;设置计数器时钟频率寄存器(CNTFRQ_EL0)。
2)第127-134行代码,如果异常级别是2,那么把向量基准地址寄存器(VBAR_EL2)设置为异常向量表的起始地址;设置协处理器陷入寄存器(CPTR_EL2),允许访问浮点和SIMD功能。
3)第135-137行代码,如果异常级别是1,那么把向量基准地址寄存器(VBAR_EL1)设置为异常向量表的起始地址;设置协处理器访问控制寄存器(CPACR_EL1),允许访问浮点和SIMD功能。
第196行代码,主处理器执行函数_main。
下面介绍第二阶段程序加载器
U-Boot分为SPL和正常的U-Boot程序两个部分,如果想要编译为SPL,需要开启配置宏“CONFIG_SPL_BUILD”。SPL是“Secondary Program Loder”的简称,即第二阶段程序加载器,第二阶段是相对于处理器里面的只读存储器的固化程序来说的,处理器启动时最先执行的只读存储器中的固化程序。
固化程序通过检测启动方式来加载第二阶段程序加载器。为什么需要第二阶段程序加载器?原因是:一些处理器内部集成的静态随机访问存储器比较小,无法装载一个完整的U-boot镜像,此时需要第二阶段程序加载器,它主要负责初始化内存和存储设备驱动,然后把正常的U-boot镜像从存储设备读到内存中执行。
2.3 函数main
函数main的代码在文件arch/arm/lib/crt0_64.S中。
67 ENTRY(_main)68 69 /*70 * Set up initial C runtime environment and call board_init_f(0).71 */72 #if defined(CONFIG_TPL_BUILD) && defined(CONFIG_TPL_NEEDS_SEPARATE_STACK)73 ldr x0, =(CONFIG_TPL_STACK)74 #elif defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)75 ldr x0, =(CONFIG_SPL_STACK)76 #elif defined(CONFIG_INIT_SP_RELATIVE)77 #if CONFIG_POSITION_INDEPENDENT78 adrp x0, __bss_start /* x0 <- Runtime &__bss_start */79 add x0, x0, #:lo12:__bss_start80 #else81 adr x0, __bss_start82 #endif83 add x0, x0, #CONFIG_SYS_INIT_SP_BSS_OFFSET84 #else85 ldr x0, =(SYS_INIT_SP_ADDR)86 #endif87 bic sp, x0, #0xf /* 16-byte alignment for ABI compliance */88 mov x0, sp89 bl board_init_f_alloc_reserve90 mov sp, x091 /* set up gd here, outside any C code */92 mov x18, x093 bl board_init_f_init_reserve94 95 #if defined(CONFIG_DEBUG_UART) && CONFIG_IS_ENABLED(SERIAL)96 bl debug_uart_init97 #endif98 99 mov x0, #0
100 bl board_init_f
101
102 #if !defined(CONFIG_SPL_BUILD)
103 /*
104 * Set up intermediate environment (new sp and gd) and call
105 * relocate_code(addr_moni). Trick here is that we'll return
106 * 'here' but relocated.
107 */
108 ldr x0, [x18, #GD_START_ADDR_SP] /* x0 <- gd->start_addr_sp */
109 bic sp, x0, #0xf /* 16-byte alignment for ABI compliance */
110 ldr x18, [x18, #GD_NEW_GD] /* x18 <- gd->new_gd */
111
112 /* Skip relocation in case gd->gd_flags & GD_FLG_SKIP_RELOC */
113 ldr x0, [x18, #GD_FLAGS] /* x0 <- gd->flags */
114 tbnz x0, 11, relocation_return /* GD_FLG_SKIP_RELOC is bit 11 */
115
116 adr lr, relocation_return
117 #if CONFIG_POSITION_INDEPENDENT
118 /* Add in link-vs-runtime offset */
119 adrp x0, _start /* x0 <- Runtime value of _start */
120 add x0, x0, #:lo12:_start
121 ldr x9, _TEXT_BASE /* x9 <- Linked value of _start */
122 sub x9, x9, x0 /* x9 <- Run-vs-link offset */
123 add lr, lr, x9
124 #if defined(CONFIG_SYS_RELOC_GD_ENV_ADDR)
125 ldr x0, [x18, #GD_ENV_ADDR] /* x0 <- gd->env_addr */
126 add x0, x0, x9
127 str x0, [x18, #GD_ENV_ADDR]
128 #endif
129 #endif
130 /* Add in link-vs-relocation offset */
131 ldr x9, [x18, #GD_RELOC_OFF] /* x9 <- gd->reloc_off */
132 add lr, lr, x9 /* new return address after relocation */
133 ldr x0, [x18, #GD_RELOCADDR] /* x0 <- gd->relocaddr */
134 b relocate_code
135
136 relocation_return:
137
138 /*
139 * Set up final (full) environment
140 */
141 bl c_runtime_cpu_setup /* still call old routine */
142 #endif /* !CONFIG_SPL_BUILD */
143 #if !defined(CONFIG_SPL_BUILD) || CONFIG_IS_ENABLED(FRAMEWORK)
144 #if defined(CONFIG_SPL_BUILD)
145 bl spl_relocate_stack_gd /* may return NULL */
146 /* set up gd here, outside any C code, if new stack is returned */
147 cmp x0, #0
148 csel x18, x0, x18, ne
149 /*
150 * Perform 'sp = (x0 != NULL) ? x0 : sp' while working
151 * around the constraint that conditional moves can not
152 * have 'sp' as an operand
153 */
154 mov x1, sp
155 cmp x0, #0
156 csel x0, x0, x1, ne
157 mov sp, x0
158 #endif
159
160 /*
161 * Clear BSS section
162 */
163 ldr x0, =__bss_start /* this is auto-relocated! */
164 ldr x1, =__bss_end /* this is auto-relocated! */
165 clear_loop:
166 str xzr, [x0], #8
167 cmp x0, x1
168 b.lo clear_loop
169
170 /* call board_init_r(gd_t *id, ulong dest_addr) */
171 mov x0, x18 /* gd_t */
172 ldr x1, [x18, #GD_RELOCADDR] /* dest_addr */
173 b board_init_r /* PC relative jump */
174
175 /* NOTREACHED - board_init_r() does not return */
176 #endif
177
178 ENDPROC(_main)
第173行代码,调用函数board_init_r(r是rear,标识后期),执行后期初始化。文件common/board_r.c定义了函数board_init_r,依次执行init_sequence_r中的每个函数,最后一个函数是run_main_loop。
2.4 函数run_main_loop
U-Boot程序初始化完成后,准备处理命令,这是通过数组init_sequence_r的最后一个函数run_main_loop实现的。
函数run_main_loop的执行流程如图所示,把主要工作委托给函数main_loop,函数main_loop的执行过程如下:
1)调用bootdelay_process以读取环境变量bootdelay和bootcmd,环境变量bootdelay定义延迟时间,即等待用户按键时间的长度;环境变量bootcmd定义要执行的命令。
U-boot程序到哪里读取环境变量?
通常我们把NOR闪存分成多个分区,其中一个分区存放U-boot程序,第二个分区存放环
境变量。U-boot程序里面的NOR的闪存驱动程序对分区信息硬编码,指定每个分区的偏移和
长度。U-boot程序从环境变量分区读取环境变量
2)调用函数autoboot_command。函数autoboot_command先调用函数abortboot,等待用户按键。如果在等待时间内用户没有按键,就调用函数run_command_list,自动执行环境变量bootcmd定义的命令。假设环境变量bootcmd定义的命令是“bootm”,函数run_command_list查找命令表,发现命令“bootm”的处理函数是do_bootm。
函数do_bootm的执行流程如图所示,把主要工作委托给函数do_bootm_states,函数do_bootm_states的执行过程如下。
1)函数bootm_start负责初始化全局变量“bootm_headers_t images”。
2)函数bootm_find_os把内核镜像从存储设备读到内存
3)函数bootm_find_other读取其他信息,对于ARM64架构,通常扁平设备树(FDT)二进制文件,该文件用来传递硬件信息给内核。
4)函数bootm_load_os把内核加载到正确的位置,如果内核镜像是被压缩过的,需要解压缩。
5)函数bootm_os_get_boot_func根据操作系统类型在数组boot_os中查找引导函数,linux内核的引导函数是do_bootm_linux。
6)第一次调用do_bootm_linux时,参数flag是BOOTM_STATE_OS_PREP,为执行linux内核做准备工作。函数do_bootm_linux(flag=BOOTM_STATE_OS_PREP)把工作委托给函数boot_prep_linux,主要工作如下。
1)分配一块内存,把扁平设备树二进制文件复制过去。
2)修改扁平设备树二进制文件,例如:如果环境变量“bootargs”指定了内核参数,那么把节点“/chosen”的属性“bootargs”设置为内核参数字符串;如果多处理系统使用自旋表启动方法,那么针对每个处理器对应的节点“cpu”,把属性“enable-method”设置为“spi-table”,把属性‘cpu-release-addr“设置为全局变量spi_table_cpu_release_addr的地址。
7)函数boot_selected_os调用函数do_bootm_linux,这是第二次调用函数do_bootm_linux,参数flag是BOOTM_STATE_OS_GO。函数do_bootm_linux(flag=BOOTM_STATE_OS_GO)调用函数boot_jump_linux,该函数跳转到内核的入口,第一个参数是扁平设备树二进制文件的起始地址,后面3个参数现在没有使用。
函数boot_jump_linux负责跳转到linux内核,执行流程如图所示:
1)调用函数smp_kick_all_cpus,如果打开了配置宏GONFIG_GICV2或者CONFIG_GIVC3,即使用通用中断控制器版本2或者版本3,那么发送中断请求以唤醒所以从处理器。
2)调用函数dcache_disable,禁用处理器的缓存和内存管理单元。
3)如果开启配置宏CONFIG_ARMV8_SWITCH_TO_EL1,表示在异常级别1执行Linux内核,那么先从异常级别3切换到异常级别2,然后切换到异常级别1,最后跳转到内核入口。
4)如果在异常级别2执行Linux内核,那么先从异常级别3切换到异常级别2,然后跳转到内核入口。
三、 内核初始化
3.1 汇编语言部分
ARM64架构的内核入口是标号_head,入口文件arch/arm64/kernel/head.S。内核各版本的细节可能不一样,框架大概是一样的。
函数__primary_switch
函数__primary_switch的主要执行流程如下:
1)调用函数__enable_mmu以开启内存管理单元
2)调用函数__primary_switched。
函数__enable_mmu的主要执行流程如下。
1)把转换表基准寄存器0(TTBR0_EL1)设置为恒等映射的页全局的起始物理地址。
2)把转换表基准寄存器1(TTBR1_EL1)设置为内核的页全局目录的起始物理地址。
3)设置系统控制寄存器(SCTLR_EL1),开启内存管理单元,以后执行程序时内存管理单元将会把虚拟地址转换成物理地址。
函数__primary_switched的执行流程如下。
1)把当前异常级别的栈指针寄存器设置0号线程内核堆栈的顶部(init_thread_union+THREAD_SIZE)。
2)把异常级别0的栈指针寄存器(SP_EL0)设置为0号线程的结构体thread_info的地址(init_task.thread_info)。
3)把向量基准地址寄存器(VBAR_EL1)设置为异常向量的起始地址(vectors)。
4)计算内核镜像的起始虚拟地址(kimage_vaddr)和物理地址的差值,保存在全局变量kimage_voffset中。
5)用0初始化内核的未初始化数据段。
6)调用C语言函数start_kernel。
3.2 C语言部分
内核初始化的C语言入口是函数start_kernel,函数start_kernel首先初始化基础设施,即初始化内核的各个子系统,然后调用rest_init。函数rest_init的执行流程如下。
1)创建1号线程,即init线程和,线程函数是kernel_init。
2)创建2号线程,即kthreadd线程,负责创建内核线程。
3)0号线程最终变成空闲线程。
init线程继续初始化,执行的主要的操作如下。
1)smp_prepare_cpus():在启动从处理器以前执行准备工作。
2)do_pre_smp_initcalls():执行在初始化SMP系统以前的早期初始化,即使用宏early_initcall注册的初始化函数
3)smp_init():初始化SMP系统,启动所有处理器。
4)do_initcalls():执行级别0~7的初始化。
5)打开控制台的字符设备文件"/dev/console",文件描述符0\1\2分别是标准输入、标准输出和标准错误,都是控制台的字符设备文件。
6)prepare_namespace():挂载根文件系统,后面装载init程序时需从存储设备上的文件系统读文件。
7)free_initmen():释放初始化代码和数据占用的内存。
8)装载init程序(U-Boot程序可以传递内核参数“init=”以指定init程序),从内核线程转换成用户空间的init进程。
级别0~7的初始化,是指使用以下宏注册的初始化函数
3.3 SMP系统的引导
自旋表(Spin Table)
- 原理:从核复位后持续轮询内存中的特定地址(自旋表),主核完成初始化后向该地址写入从核的启动指令地址,触发从核跳转执行系统代码。
- 硬件依赖:仅需基础内存管理单元(MMU),无需专用硬件支持。
电源状态协调接口(PSCI)
- 原理:主核通过固件(如ARM Trusted Firmware)调用标准接口(如
PSCI_CPU_ON
),由固件控制从核的电源状态和启动入口地址。 - 硬件依赖:需芯片支持PSCI协议且预装兼容固件(如ATF)。
- 原理:主核通过固件(如ARM Trusted Firmware)调用标准接口(如
ACPI停车协议(ACPI Parking Protocol)
- 原理:主核解析ACPI表获取从核状态寄存器地址,写入启动指令唤醒从核,遵循x86平台的电源管理标准。
- 硬件依赖:依赖ACPI兼容的硬件和操作系统驱动(如Linux ACPI子系统)。
特性 | 自旋表(Spin Table) | PSCI | ACPI停车协议 |
---|---|---|---|
控制主体 | 主核直接操作内存 | 固件代理电源管理 | 操作系统通过ACPI驱动控制 |
硬件依赖 | 低(仅需MMU) | 高(需PSCI兼容固件) | 高(需ACPI兼容硬件) |
功耗效率 | ❌ 轮询导致高功耗 | ✅ 休眠管理优化能效 | ️ 中等(依赖OS电源策略) |
启动延迟 | ⚠️ 较高(等待轮询) | ✅ 低(事件触发唤醒) | ⚠️ 中等(需解析ACPI表) |
适用场景 | 低成本嵌入式设备(如IoT) | 移动设备/通用ARM平台 | ARM服务器或x86/ARM混合架构 |
系统兼容性 | ✅ 所有ARM架构 | ✅ ARMv8+ | ⚠️ 需操作系统支持ACPI |
四、 init进程
嵌入式设备常用init程序对比表
特性 | BusyBox init | System V init | systemd |
---|---|---|---|
复杂性 | 低(仅Shell脚本)12 | 中(分级启动脚本)812 | 高(二进制+配置文件)512 |
启动速度 | 快(轻量级)12 | 慢(串行启动)812 | 中(并行启动)512 |
依赖环境 | 需ash Shell支持12 | 需ash/bash Shell12 | 需glibc库12 |
配置文件 | /etc/inittab (简化版)12 | /etc/inittab +/etc/rc.d 6 | .target 单元文件58 |
进程管理 | 基础服务控制12 | 脚本手动管理6 | 内置守护进程监控58 |
典型应用场景 | 资源受限的嵌入式设备12 | 传统嵌入式Linux12 | 复杂嵌入式系统(如树莓派)5 |
- BusyBox init:作为嵌入式领域的“瑞士军刀”,集成基础功能且体积极小,适合内存小于32MB的设备。
- System V init:通过运行级别(0-6)管理服务状态,但启动效率较低。
- systemd:支持并行启动和服务依赖管理,但占用资源较多(约34MB)。
现代嵌入式系统(如Yocto项目)通常允许在编译时选择这三种实现。