三明市网站建设_网站建设公司_C#_seo优化
2026/1/16 12:43:00 网站建设 项目流程

ARM64与AMD64固件开发入门:从零理解启动第一阶段

你有没有想过,按下电源键的瞬间,CPU是如何“醒来”的?为什么有的设备开机只需几秒,而有的却要等上十几秒?这一切的秘密,都藏在固件启动的第一阶段——那个操作系统尚未加载、连内存都没初始化的“混沌初开”时刻。

在这个阶段,没有C库、没有堆栈、甚至没有RAM可用。一切都要靠开发者亲手搭建。而ARM64和AMD64,作为当今最主流的两种64位架构,在这条“从0到1”的路上,走出了截然不同的路径。

本文不讲空泛理论,也不堆砌术语。我们将像拆解一台老式收音机一样,一层层拨开ARM64与AMD64启动流程的外壳,带你真正看懂:
- 上电后第一条指令到底执行了什么?
- 没有内存时程序如何运行?
- 为什么一个用汇编写死地址,另一个却能跑C代码?
- 实际开发中哪些坑必须避开?

无论你是嵌入式新手,还是想深入底层的系统工程师,这篇文章都会让你对“启动”这件事,有全新的认知。


ARM64是怎么“醒过来”的?

起点:从一片黑暗中点亮火把

想象一下,芯片刚上电,内部就像一座断电的城市——没有路灯(时钟未启)、没有道路(MMU关闭)、也没有通信(外设未初始化)。唯一的光源,是预埋在芯片ROM里的那一小段代码。

对于ARM64来说,这个起点通常是物理地址0x00000000或者某个映射到片内ROM的地址。CPU复位后,会自动跳转到这里开始取指执行。这段代码就是BL1(Boot Loader Stage 1),它必须足够小、足够快,并且完全独立于外部DRAM。

更重要的是,ARM64一上来就进入了最高权限模式——EL3(Exception Level 3)。这是专为安全监控设计的特权层级,也是TrustZone可信执行环境的基石。

📌关键点:ARM64不是一步步“爬上去”的,而是直接站在顶峰(EL3),然后决定要不要下来。

第一步做什么?别急着开MMU!

很多初学者有个误解:以为启动第一步就是开启MMU和虚拟内存。错!恰恰相反,第一步反而是确保MMU处于关闭状态

因为在没有页表之前,任何对虚拟地址的访问都会导致异常。所以BL1的第一件事,是读取系统控制寄存器SCTLR_EL3,并清除其中的M位(bit 0)来明确禁用MMU:

mrs x0, sctlr_el3 // 读取当前SCTLR值 bic x0, x0, #(1 << 0) // 清除M位 → 关闭MMU msr sctlr_el3, x0 // 写回寄存器

同理,数据缓存(C bit)、对齐检查(A bit)等也都会被暂时关闭,以进入一个确定、可控的状态。

栈都放哪儿?SRAM就是临时家

既然DRAM还没初始化,那函数调用用的栈放哪里?答案是:片上SRAM或内部缓存

ARM64要求开发者在BL1阶段手动设置栈指针SP。通常做法是将SP指向一块已知可用的SRAM区域高地址(向下增长):

ldr x0, =stack_top mov sp, x0

这块内存不需要初始化,只要物理存在即可。这也是为什么BL1必须精简——它只能使用有限的静态空间,不能依赖.data.bss段(这些需要后续清零才能用)。

异常向量表:给中断安个“门牌号”

接下来要设置的是异常向量表基址寄存器 VBAR_EL3。你可以把它理解为一张“紧急联系电话表”,当发生复位、中断、异常时,CPU就知道该去哪找处理程序。

ldr x0, =vector_table msr vbar_el3, x0

这个表本身可以放在SRAM里,长度一般为2KB(每个异常入口占128字节)。一旦设好,系统就有了基本的容错能力。

多核怎么协调?主核干活,从核睡觉

现代ARM64处理器都是多核的。但启动时并不是所有核心一起上电,而是由主核(Primary Core)单独完成初始化,其他从核保持低功耗等待状态。

常用机制是让从核执行wfe(Wait For Event)指令,进入休眠,直到主核通过sev(Send Event)唤醒它们。这种模式不仅节能,还能避免资源竞争。

更高级的做法是通过PSCI(Power State Coordination Interface)标准接口统一管理核的启停,实现跨平台兼容。

最终目标:把接力棒交给BL2

BL1的任务很简单:准备好最基本的运行环境,然后把更大的舞台留给BL2。

典型动作包括:
- 初始化时钟和看门狗
- 配置GPIO用于调试输出
- 运行DDR控制器驱动,完成内存训练
- 将BL2镜像从Flash复制到DRAM
- 跳转至BL2入口

此时,系统终于有了可用内存,可以启用缓存、开启MMU,进入更复杂的引导阶段。

💡经验之谈:BL1越短越好,建议控制在几KB以内;所有操作应尽量无副作用,便于调试。


AMD64又是怎么“开机”的?

完全不同的起点:固定地址0xFFFFFFF0

如果说ARM64的启动像是“灵活选址创业”,那么AMD64更像是“按图索骥”。

所有amd64 CPU上电后,都会自动将CS:EIP设置为0xF000:FFF0,对应物理地址0xFFFFFFF0——距离4GB顶端仅16字节。这个地方永远存放着一条跳转指令:

jmp far 0xe0000 ; 跳往SPI Flash中的固件代码

这16字节的空间极其珍贵,几乎不做任何实质性工作,只是一个“引子”,把执行流导向真正的固件存储区(通常是主板上的SPI Flash芯片)。

实模式起步:回到1980年代的技术栈

有趣的是,尽管我们谈论的是64位架构,但amd64 CPU一开始运行的却是16位实模式。这是一种向后兼容的设计遗产,源自Intel 8086时代。

在这种模式下:
- 地址空间受限于20位(最大1MB)
- 没有分页机制
- 使用段:偏移方式寻址

但这没关系,因为这一阶段的目标不是功能完整,而是尽快建立起一个可运行C代码的环境。

CAR:用缓存当内存使

这里出现了一个非常聪明的技术——Cache-as-RAM(CAR)

在DRAM尚未初始化前,传统方案只能靠汇编+极少量内部寄存器工作。但amd64平台利用L1/L2缓存未被占用的特点,将其配置为静态分配的RAM替代品

具体操作如下:
1. 禁用缓存(设置CR0.CD=1)
2. 启用缓存但锁定为WB模式
3. 手动映射一段缓存行作为堆栈和全局变量区

这样一来,虽然没有DDR,也能跑起C函数了!

优势:开发者可以用结构化代码编写PEI模块,大幅提升可维护性。
限制:CAR空间通常不超过128KB,栈深度需严格控制。

PEI阶段:Intel平台的核心过渡期

PEI(Pre-EFI Initialization)是UEFI框架下的第一个正式阶段,相当于ARM64中的BL1,但它更加模块化。

它的主要职责包括:
- 初始化北桥/南桥芯片组
- 加载微码补丁(修复CPU硬件缺陷)
- 配置内存控制器
- 调用FSP-M(Firmware Support Package - Memory)

其中,FSP-M是最关键的组件,由Intel或SoC厂商提供二进制blob,负责具体的DDR训练与时序调整。开发者只需按规范调用,无需关心底层细节。

EFI_STATUS PeiMain(IN CONST EFI_PEI_SERVICES **PeiServices) { MemoryInit(PeiServices); // 调用FSP-M初始化内存 InstallPpi(PeiServices, &mMemoryInstalledPpiDesc); JumpToNextPhase(PeiServices); // 跳转至DXE }

一旦内存初始化完成,系统就会切换到正常RAM运行,进入DXE(Driver Execution Environment)阶段,开始加载各种驱动和服务。

多核启动:靠“广播消息”叫醒兄弟

amd64的多核唤醒机制称为INIT-SIPI-SIPI协议

  1. 主核发送 INIT IPI(处理器间中断),通知所有从核准备启动;
  2. 主核发送 SIPI(Startup IPI),指定一个向量地址;
  3. 从核收到后,跳转到该地址执行初始化代码;
  4. 可重复一次SIPI确保稳定。

这种方式依赖APIC总线通信,属于x86特有的中断协调机制。


对比总结:两种哲学,一条终点

维度ARM64AMD64
初始执行模式AArch64 EL3(64位)实模式(16位)
启动地址可配置(ROM/SRAM)固定0xFFFFFFF0
内存前运行环境片内SRAMCache-as-RAM
权限模型EL3 → EL2 → EL1 → EL0Ring 0 ~ 3(保护模式)
中断模型异常级别 + 向量表IDT + GDT 段机制
内存初始化方式自研DDR驱动 or DDRFWFSP-M / MRC
主流固件框架TF-A, U-Boot SPLUEFI + EDK II
多核唤醒机制PSCI CPU_ON / WFESIPI消息广播

表面上看,两者差异巨大:
- ARM64强调清晰的权限隔离与安全性,适合构建可信计算基础;
- AMD64则偏向生态兼容与工程效率,借助标准化接口降低开发门槛。

但本质上,它们都在解决同一个问题:如何在一个“三无”环境(无内存、无OS、无服务)中,亲手造出第一个“有秩序”的世界


开发实战:避坑指南与调试技巧

常见问题1:卡在DDR初始化?

这是最常见的启动失败场景。

ARM64排查思路:
  • 检查DDR频率、时序参数是否匹配颗粒规格
  • 使用示波器测量时钟信号完整性
  • 在关键节点添加串口打印(即使只是点亮一个GPIO灯)
  • 启用JTAG调试,单步跟踪内存训练过程
AMD64常见原因:
  • FSP-M返回错误码(如0x1A表示内存训练失败)
  • SPI Flash映射错误导致固件读取异常
  • 微码未正确加载导致CPU行为异常

可通过BIOS日志(POST Code)判断故障阶段。

常见问题2:从核无法启动?

ARM64检查项:
  • 是否正确实现了PSCI SMC处理程序?
  • 从核入口地址是否已被正确加载到指定位置?
  • 是否遗漏了GIC(中断控制器)配置?
AMD64注意点:
  • APIC ID分配是否冲突?
  • SIPI向量地址是否落在可执行范围?
  • CAR区域是否覆盖到了从核代码?

调试利器推荐

  1. QEMU仿真先行
    ```bash
    # 模拟ARM64启动
    qemu-system-aarch64 -M virt -cpu cortex-a57 -nographic -bios bl1.bin

# 模拟x86_64 UEFI启动
qemu-system-x86_64 -bios OVMF.fd -nographic -drive file=disk.img,format=raw
```

  1. 串口日志必接
    无论是UART0还是PL011,务必尽早初始化并输出日志。哪怕只打印一个字符,也能帮你确认代码走到哪一步。

  2. 符号表辅助分析
    编译时保留.symtab,配合objdump -d反汇编,可在崩溃时定位具体指令。

  3. 使用EDK II或TF-A参考实现
    不要从零造轮子。Trusted Firmware-A 和 Tianocore EDK II 提供了经过验证的启动模板,适合作为基础进行裁剪。


写在最后:掌握启动,才算真正入门系统编程

当你第一次看到自己的汇编代码成功跳转到C函数,第一次看到串口输出“DRAM init done”,那种成就感远超普通应用开发。

而这,正是系统级程序员的独特乐趣。

ARM64与AMD64虽然路径不同,但都告诉我们一个道理:真正的掌控力,来自于对最底层逻辑的理解

未来,无论是投身国产芯片替代、参与云服务器定制,还是研究RISC-V自研架构,今天所学的这些原理,都会成为你手中最锋利的工具。

如果你正在尝试移植U-Boot、修改TF-A,或者调试UEFI启动失败的问题,不妨停下来问问自己:

“我现在处在启动流程的哪一环?下一步需要建立什么环境?”

也许答案,就在你已经读过的某一行汇编里。

欢迎在评论区分享你的启动调试经历,我们一起破解那些“黑盒”背后的真相。

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

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

立即咨询