阳泉市网站建设_网站建设公司_SSL证书_seo优化
2026/1/17 8:13:41 网站建设 项目流程

从零开始:深入理解 arm64-v8a 系统启动的第一阶段

你有没有想过,一块通电的开发板是如何“活”起来的?当按下电源键,CPU 并不会直接运行 Linux 或 Android——它首先得靠一段隐藏在最底层的代码,一步步把自己“扶起来”。对于如今广泛应用于手机、服务器和嵌入式设备的arm64-v8a架构来说,这个“扶自己站起来”的过程,就是我们常说的系统启动第一阶段

这不仅是 Bootloader 的起点,更是整个系统可信运行的根基。如果你正准备移植 U-Boot、调试启动失败问题,或者想深入理解安全启动(Secure Boot)机制,那么本文将带你从零开始,亲手揭开 arm64-v8a 启动第一阶段的神秘面纱


启动之前:CPU 上电那一刻发生了什么?

系统上电后,CPU 处于一个“空白但确定”的状态:寄存器内容未定义(除了 PC),内存为空白,缓存关闭,中断禁用。此时,CPU 必须知道“第一条指令从哪里取”——这就是复位向量(Reset Vector)的作用。

在 arm64-v8a 架构中,复位向量地址由 SoC 厂商决定,常见的有:

  • 0x0000_0000(低端映射)
  • 0xFFFF_0000(高端映射)

这个地址通常映射到一片只读存储器(ROM),里面固化了厂商提供的Boot ROM代码。它负责最基本的初始化,并加载外部存储器(如 eMMC、SPI Flash)中的第一阶段引导程序(BL1)。而 BL1,正是我们要深入剖析的核心。

💡关键点:真正的用户可控启动代码,往往不是从复位向量开始执行,而是由 Boot ROM 加载并跳转而来。但我们仍需掌握向量表结构,因为它是异常处理的基础。


arm64 的权力金字塔:异常级别 EL 与安全世界

arm64-v8a 最大的设计亮点之一,是它的分层特权架构——通过Exception Level(EL)Secure/Non-secure World实现精细化控制。

四级权限:从内核到安全监控

异常级别名称典型用途
EL0用户级应用程序运行
EL1内核级操作系统内核
EL2虚拟机监控级Hypervisor
EL3安全监控级安全固件(如 ATF)、世界切换

系统上电后,绝大多数 arm64-v8a 处理器会自动进入EL3,并且处于Secure World。这是权限最高的层级,可以访问所有硬件资源,包括安全寄存器和 GIC(通用中断控制器)。

为什么是 EL3?因为它足够“高”,能完成系统初始化;又足够“安全”,适合做信任根(Root of Trust)。

安全世界:TrustZone 的核心

arm64 支持两种运行世界:

  • Secure World:运行可信执行环境(TEE),如 OP-TEE
  • Non-secure World:运行普通操作系统(如 Linux)

启动流程通常是:

Power-on → EL3 (Secure) → 初始化 → 切换至 EL1 (Non-secure)

这种设计让安全固件有机会验证后续镜像(如 BL2、kernel)的完整性,再决定是否放行,从而构建一条可信启动链


复位向量表:CPU 的第一张地图

虽然上电后实际执行的是 Boot ROM,但我们编写的 BL1 仍然需要提供一个异常向量表,告诉 CPU 当各种异常发生时该去哪。

arm64-v8a 规定,异常向量表必须按 128 字节对齐,每个异常入口占 16 字节,共 16 个条目。复位向量位于第一个条目。

下面是一个极简的向量表示例:

.section ".vector_table", "ax" .align 7 // 128-byte alignment .globl _reset_vector _reset_vector: b el3_setup // 复位 -> 跳转到 EL3 初始化 // 其他异常暂时指向无限等待 _invalid_exception: wfe b _invalid_exception // 填充剩余向量项 .space 15 * 16, 0

这段代码非常关键:当 Boot ROM 完成加载后,会跳转到_reset_vector,然后立即进入el3_setup,正式开启我们的掌控。


EL3 初始化:搭建舞台的第一步

进入 EL3 后,我们手里的“工具”还很少。没有栈、没有 C 环境、没有内存管理。第一步,就是给自己搭个“脚手架”。

1. 设置栈指针(SP)

没有栈,就无法调用函数或保存局部变量。我们必须手动设置 SP 指向一块可用的 RAM 区域,比如片上 SRAM(IRAM)。

el3_setup: ldr x0, =0x0400FFFF // 假设 IRAM 地址为 0x04000000,栈向下增长 mov sp, x0

从此,我们可以安全地调用 C 函数了。

2. 关闭中断与异常

在初始化完成前,任何中断都可能导致不可预知的行为。使用DAIF寄存器一次性屏蔽所有异常:

msr daifset, #0xF // D=1, A=1, I=1, F=1: 屏蔽调试、异步错误、IRQ、FIQ

3. 配置系统控制寄存器 SCTLR_EL3

SCTLR 控制着 MMU、缓存、对齐检查等关键功能。启动初期,我们通常先关闭它们,避免复杂性:

ldr x0, =0x30C50888 msr sctlr_el3, x0

这个值的含义如下:
-bit[2]:WMMX = 0 → 禁用写缓冲合并
-bit[12]:I = 0 → 禁用指令缓存
-bit[29]:M = 0 → 禁用 MMU
- 其他位使能 ZCR、禁用 WXN 等

具体配置需参考芯片手册,但原则是:先关后开,逐步启用

4. 设置安全控制寄存器 SCR_EL3

SCR_EL3 决定了下一跳的目标环境:

ldr x0, =0x00000018 // NS=1, RW=1 msr scr_el3, x0
  • NS=1:下一级运行在 Non-secure World
  • RW=1:下一级以 AArch64 模式运行

只有 EL3 能修改 SCR_EL3,这也是它作为“守门人”的特权。


如何跳到下一个异常级别?ERET 的魔法

现在,我们已经准备好切换到 EL1(操作系统内核即将运行的地方)。但这不能简单地b main,而必须通过ERET指令完成跨 EL 跳转。

ERET 的工作原理是:从SPSR_EL3ELR_EL3寄存器中恢复处理器状态和目标地址。

// 设置下一级的处理器状态 ldr x0, =0x3c9 // M[3:0]=1101 (EL1t), D/A/I/F=1 (屏蔽异常) msr spsr_el3, x0 // 设置下一级的程序计数器 adr x0, _main_c msr elr_el3, x0 // 执行 ERET,跳转到 EL1 eret

执行eret后,CPU 会:
- 切换到 EL1
- 进入 AArch64 模式
- 屏蔽中断
- 从_main_c开始执行

整个过程干净利落,且符合架构规范。


早期内存管理:没有 MMU 怎么办?

在 MMU 启用前,所有内存访问都是物理地址直连。我们使用的内存只能是物理内存中的一块静态区域,比如 IRAM 或 TCM。

栈空间分配

在链接脚本中预留一段内存作为栈:

/* linker.ld */ _stack_start = 0x04008000; _stack_end = 0x0400FFFF; SECTIONS { .text : { *(.vector_table) *(.text*) } > IRAM .stack (_stack_start) : { . = . + (_stack_end - _stack_start); } > IRAM }

这样,栈大小固定为约 32KB,足够支撑早期初始化。

BSS 清零与数据段复制

C 语言要求.bss段清零,.data段从 Flash 复制到 RAM。这些工作必须由我们手动完成。

// crt.c extern unsigned char __data_start__, __data_end__; extern unsigned char __rom_data_start__; extern unsigned char __bss_start__, __bss_end__; void c_runtime_init(void) { // 复制 .data 段 unsigned char *src = &__rom_data_start__; unsigned char *dst = &__data_start__; while (dst < &__data_end__) { *dst++ = *src++; } // 清零 .bss 段 dst = &__bss_start__; while (dst < &__bss_end__) { *dst++ = 0; } main(); }

链接器脚本需导出这些符号:

__data_start__ = ADDR(.data); __data_end__ = ADDR(.data) + SIZEOF(.data); __rom_data_start__ = LOADADDR(.data); __bss_start__ = ADDR(.bss); __bss_end__ = ADDR(.bss) + SIZEOF(.bss);

完成后,我们终于可以在 C 语言中自由驰骋了。


启动第一阶段完整流程图解

整个 BL1 的执行流程可归纳为:

上电复位 ↓ CPU 从复位向量取指 ↓ 跳转到 el3_setup(汇编) ↓ 设置 SP,关闭中断 ↓ 配置 SCTLR、SCR 等寄存器 ↓ 调用 c_runtime_init(C 函数) ↓ 复制 .data,清零 .bss ↓ 初始化串口(输出调试信息) ↓ 加载 BL2 镜像到 RAM ↓ 设置 SPSR_EL3 和 ELR_EL3 ↓ ERET 跳转至 EL1(BL2 入口)

这一阶段虽短,却决定了整个系统的生死。任何一个环节出错,都会导致“黑屏死机”。


实战技巧:如何调试启动失败?

启动阶段几乎没有调试工具可用,但我们可以借助一些“土办法”:

  1. 尽早初始化串口:输出 “Hello from EL3” 是最有效的“心跳信号”
  2. 使用看门狗:防止卡死,强制重启便于观察
  3. LED 闪烁编码:不同闪烁模式代表不同执行阶段
  4. JTAG 调试:配合 DS-5 或 OpenOCD,单步跟踪汇编代码

记住:启动代码越早输出信息,越容易定位问题


为什么这个阶段如此重要?

掌握启动第一阶段,意味着你具备了以下能力:

  • 自主构建系统:不再依赖现成 SDK,可以从零搭建最小可运行环境
  • 深度调试能力:面对“开机无反应”问题,能精准定位是硬件、固件还是配置问题
  • 安全启动实现:在 EL3 中加入镜像签名验证,构建可信根
  • 跨平台移植基础:理解通用流程后,移植到新 SoC 更加得心应手

无论是开发定制 Bootloader、移植 U-Boot,还是研究 TEE 安全机制,这都是绕不开的基本功。


如果你正在学习嵌入式 Linux、RTOS 或固件安全,不妨动手写一个最简 BL1:从复位向量开始,设置栈,跳转 C 函数,最后eret到 EL1。哪怕只是点亮一个 LED,那也是你真正“掌控硬件”的开始。

欢迎在评论区分享你的启动调试经历,或者提出你在移植过程中遇到的坑。我们一起,把系统“从零启动”这件事,做到极致。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询