news 2026/3/2 2:53:52

超详细版ARM64内核启动过程:从汇编到C环境搭建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
超详细版ARM64内核启动过程:从汇编到C环境搭建

从零开始看懂ARM64内核启动:一场汇编与C的交接仪式

你有没有想过,一块通电的ARM64芯片,是如何从第一条机器指令一步步走到printf("Hello World\n");的?

这不像写个“Hello, World”那么简单。在操作系统真正开始运行之前,CPU其实处于一种“裸奔”状态——没有栈、没有全局变量、甚至连内存映射都还没建立。而这一切,都要靠一段精巧的汇编代码来完成。

本文不讲空泛理论,也不堆砌术语,而是带你亲手走一遍ARM64平台上的内核启动全流程,从复位向量到main()函数调用,从EL3特权级切换到MMU开启那一刻的惊心动魄。你会发现,这段过程不仅是技术细节的堆叠,更是一场精心设计的“权力交接”。


启动的第一步:谁说了算?异常级别(EL)是关键

当你的ARM64设备按下电源键,CPU并不会直接跳进C语言世界。它首先进入的是一个叫EL3(Exception Level 3)的最高特权模式。

为什么是EL3?因为它掌管着整个系统的信任根(Root of Trust)。你可以把它理解为“保安队长”,负责检查所有后续加载的代码是否可信。像ARM Trusted Firmware(ATF)中的BL1阶段,就是在这个层级运行的。

ARM64有四个异常级别:

EL权限等级典型用途
EL3最高安全监控、TrustZone切换
EL2次高虚拟机管理器(Hypervisor)
EL1内核级Linux内核
EL0用户级应用程序

重点来了:系统上电后默认运行在EL3,但Linux内核要跑在EL1。所以整个启动过程的核心任务之一,就是安全地从EL3降级到EL1

这个降级不是简单跳转就能完成的。你必须设置好两个关键寄存器:
-ELR_EL3:指定降级后要执行的第一条指令地址;
-SPSR_EL3:定义目标EL的处理器状态(如中断使能、异常级别等);

然后通过一条eret指令触发异常返回,硬件才会真正切换上下文。否则,轻则死机,重则被安全机制锁住。


异常来了怎么办?向量表决定第一响应人

CPU不可能预知什么时候会发生中断或错误。为了应对这些突发事件,ARM64设计了一套叫做异常向量表(Vector Table)的机制。

想象一下:CPU就像一个值班室,每当发生异常(比如复位、未定义指令、IRQ中断),它就会根据事件类型自动跳转到对应的处理函数入口。这些入口集中在一起,形成一张“应急响应清单”——这就是向量表。

每个异常条目占128字节,共支持16种情况,整张表大小固定为2KB,并且必须按2KB对齐。

举个例子,当芯片上电复位时,硬件会自动跳转到向量表的第一个位置(偏移0x000),那里应该放一条指向reset_handler的跳转指令。

我们来看一段真实的汇编实现:

.section ".vectors", "ax" .align 11 /* 2^11 = 2048 bytes alignment */ vector_table_el3: b reset_handler_el3 /* Reset */ b undefined_handler_el3 /* Undefined instruction */ b supervisor_call_el3 /* SVC */ b prefetch_abort_el3 /* Prefetch abort */ b data_abort_el3 /* Data abort */ b . /* Reserved */ b interrupt_handler_el3 /* IRQ */ b fast_interrupt_el3 /* FIQ */

紧接着,我们在reset_handler_el3中做最基础的初始化:

reset_handler_el3: mov x21, #0x80000000 /* 假设栈起始地址 */ mov sp, x21 /* 设置栈指针 */ ldr x0, =__bss_start ldr x1, =__bss_end sub x2, x1, x0 bl __memset_zero /* 清BSS段 */ b c_setup_main /* 跳转到C环境准备函数 */

看到这里你可能会问:为什么不直接写C代码?
答案很现实:C语言依赖运行时环境,而此时环境还不存在。


C语言为何不能“裸上”?三大前提缺一不可

想让C函数正常工作,至少需要满足三个条件:

  1. 栈指针(SP)已就位
    所有局部变量、函数调用帧都依赖栈空间。AArch64 ABI要求栈在调用C函数前保持16字节对齐。

  2. BSS段已被清零
    .bss段存放未初始化的全局变量(如int buf[1024];)。虽然不占用镜像空间,但在程序启动前必须清零,否则读取结果不可控。

  3. 数据段若需重定位,必须搬移
    如果链接地址 ≠ 实际加载地址(例如内核被加载到DRAM但期望运行在另一区域),就需要手动复制.data段。

其中,栈和BSS初始化是最基本、最普遍的要求,哪怕是最小的裸机程序也不能跳过。

下面这段汇编完成了核心准备工作:

.globl c_setup_main c_setup_main: ldr x0, =stack_top mov sp, x0 /* 设置栈顶 */ ldr x0, =__bss_start ldr x1, =__bss_end sub x2, x1, x0 /* 计算长度 */ mov x3, #0 1: cbz x2, 2f str x3, [x0], #8 sub x2, x2, #8 b 1b 2: bl kernel_main /* 终于可以调C了! */ b .

一旦bl kernel_main执行成功,你就正式进入了C的世界。


MMU开启前夜:页表怎么建?虚拟地址如何映射?

接下来是整个启动过程中最具挑战性的一步:启用MMU(内存管理单元)

MMU的作用是将虚拟地址翻译成物理地址,并实施访问权限控制。但在启用之前,你得先准备好页表结构。

ARM64通常采用四级页表(可配置为三级),每级使用9位索引,加上最低12位页内偏移,构成48位虚拟地址空间。

最关键的几个系统寄存器如下:

寄存器功能说明
TTBR0_EL1存放用户/内核页表基地址
TCR_EL1配置VA/PA宽度、页粒度、共享属性
MAIR_EL1定义内存类型(如Normal WB、Device memory)
SCTLR_EL1主控开关,M=1开启MMU,C=1开启缓存

假设我们要建立一个简单的恒等映射(identity mapping),即虚拟地址 == 物理地址,适用于早期启动阶段。

先看TCR_EL1的典型配置(48位地址空间,4KB页):

uint64_t tcr_val = (16ull << 37) | // T0SZ = 16 → 48-bit VA (2ull << 32) | // TG0 = 2 → 4KB granule (1ull << 20) | // SH0 = 1 → Inner Shareable (4ull << 16) | // ORGN0 = 4 → Outer Write-back Write-Allocate (4ull << 12); // IRGN0 = 4 → Inner Write-back Write-Allocate write_sysreg(tcr_val, TCR_EL1);

再设置页表基址:

extern uint64_t level0_page_table[]; write_sysreg((uint64_t)level0_page_table, TTBR0_EL1);

最后别忘了告诉MMU哪些内存类型对应什么行为:

// MAIR: Memory Attribute Indirection Register uint64_t mair_val = (0xff << 0) | // Attr0: Normal WB Cacheable (0x00 << 8); // Attr1: Device-nGnRnE write_sysreg(mair_val, MAIR_EL1);

一切就绪后,就可以尝试开启MMU了:

uint64_t sctlr = read_sysreg(SCTLR_EL1); sctlr |= (1 << 0); // M bit: enable MMU sctlr |= (1 << 2); // C bit: enable cache write_sysreg(sctlr, SCTLR_EL1); // ⚠️ 此刻之后的所有地址访问都将经过MMU转换!

🔥致命陷阱提醒:如果你的页表没有覆盖当前代码所在的物理地址,开启MMU瞬间就会因取指失败而崩溃。务必确保恒等映射包含当前运行区域!


和x86_64比一比:架构哲学的不同体现

很多人熟悉x86的启动流程:实模式 → 保护模式 → 长模式。那条长长的兼容链背后,是几十年历史包袱的积累。

而ARM64完全不同。它是原生64位设计,没有分段机制,也没有实模式。它的启动哲学更接近“自上而下”的权限管控:

对比维度x86_64ARM64
初始模式实模式(16位)直接运行在EL3(64位)
地址空间分段 + 分页平坦虚拟地址 + 多级页表
异常处理IDT(中断描述符表)向量表(每EL独立)
权限模型RING0 ~ RING3EL0 ~ EL3
安全扩展SMX/SMMTrustZone(基于EL3的安全世界切换)
启动固件BIOS/UEFIATF、U-Boot等

可以说,x86是在不断挣脱过去的束缚,而ARM64是从一开始就选择了简洁与可控

这也解释了为什么现代嵌入式系统、服务器甚至苹果Mac都在转向ARM架构——它更适合构建确定性高、安全性强的系统。


实战中的常见坑点与调试秘籍

即便你知道了全部流程,在实际移植或调试中仍可能踩坑。以下是几个高频问题及解决方案:

❌ 问题1:板子上电后毫无反应,串口无输出

排查思路
- 是否正确设置了向量表地址?检查VBAR_EL3是否指向正确的基址;
- 栈指针是否指向无效RAM?确认DDR是否已初始化;
- 是否关闭了看门狗定时器?某些SoC默认开启会导致快速复位。

❌ 问题2:BSS清零后全局变量仍是随机值

原因:链接脚本中未正确定义__bss_start__bss_end符号。

解决方法:在linker.lds中明确声明:

.bss : { __bss_start = .; *(.bss) *(COMMON) __bss_end = .; } > RAM

❌ 问题3:开启MMU后立即死机

最大可能:页表未覆盖当前代码运行区域。

调试技巧
- 在开启MMU前插入汇编断言:
armasm dsb sy isb
- 使用JTAG调试器查看PC停在哪条指令;
- 确保恒等映射范围足够大(建议至少覆盖前64MB)。

✅ 秘籍:早期打印(early printk)救星登场

在MMU和设备驱动未就绪前,可以通过直接操作串口寄存器输出调试信息:

void early_uart_putc(char c) { volatile uint32_t *uart_reg = (void*)0x1c090000; while ((*uart_reg & (1<<6)) == 0); // 等待发送缓冲空 *uart_reg = c; }

哪怕只是输出一个'.',也能帮你判断代码是否执行到了某一点。


整体流程串起来:从上电到内核主循环

让我们把上述所有环节串联成一个完整的启动链条:

[Power On] ↓ CPU从Boot ROM执行第一条指令(通常位于0x0) ↓ 跳转至Reset Vector → 进入 vector_table_el3 ↓ reset_handler_el3 设置 SP、清BSS ↓ 初始化时钟、串口、DDR控制器(可能由BL1完成) ↓ 建立恒等映射页表,配置TCR/MAIR/TTBR ↓ 开启MMU + Cache(注意一致性操作) ↓ 设置ELR_EL3 = kernel_entry, SPSR_EL3 = target_state ↓ eret → 切换至EL1,跳转到kernel_entry ↓ 继续执行head.S中的setup_arch()等初始化 ↓ 调用start_kernel() → 进入C主导的内核初始化 ↓ 设备树解析、调度器启动、内存子系统初始化 ↓ fork出init进程 → 用户空间启动

这一连串动作,如同精密齿轮咬合,任何一环断裂都会导致系统无法启动。


掌握这项技能,你能做什么?

深入理解ARM64启动流程,不只是为了应付面试题。它实实在在能帮你解决以下问题:

  • 移植Linux内核到新SoC平台:你需要修改head.S、调整页表布局、适配新的中断控制器;
  • 开发定制Bootloader:无论是简化版U-Boot还是自制OS引导器,都需要亲手搭建C环境;
  • 调试早期崩溃(Oops in head.S):知道每一步发生了什么,才能精准定位问题;
  • 实现安全启动(Verified Boot):利用EL3进行签名验证,防止恶意固件注入;
  • 优化启动时间:剔除不必要的初始化步骤,提升产品响应速度。

更重要的是,随着RISC-V等新生代架构崛起,其启动模型也大量借鉴了ARM64的设计思想——异常级别、向量表、页表初始化、C环境搭建,这些模式正在成为通用范式。


结尾彩蛋:未来的演进方向

ARM并未止步于此。近年来推出的新特性进一步丰富了启动生态:

  • Realm Management Extension (RME):在原有TrustZone基础上增加“领域(Realm)”概念,实现更强的隔离;
  • Scalable Vector Extension (SVE):启动时需检测CPU是否支持向量扩展;
  • Memory Tagging Extension (MTE):启用后可在早期内存分配中加入标签检测,防溢出攻击。

尽管底层机制会变,但那个永恒的主题不会改变:如何从汇编走向C,如何从裸金属走向操作系统

当你下次看到kernel_main()被调用时,不妨停下来想一想:在这之前,有多少行汇编代码默默完成了使命?

如果你在实践中遇到具体问题,欢迎留言交流。我们可以一起分析启动日志、反汇编崩溃点,甚至手把手教你写一个最小可运行的ARM64裸机程序。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/1 2:23:29

2025年Jable视频下载新方案:3分钟搞定本地保存

2025年Jable视频下载新方案&#xff1a;3分钟搞定本地保存 【免费下载链接】jable-download 方便下载jable的小工具 项目地址: https://gitcode.com/gh_mirrors/ja/jable-download 还在为无法保存喜欢的Jable视频而烦恼吗&#xff1f;今天介绍一款完全免费的本地下载工具…

作者头像 李华
网站建设 2026/2/21 9:14:40

Multisim在实验课中数据库异常的实战案例分析

一次“Multisim无法访问数据库”的教学事故&#xff0c;让我们重新认识实验室软件的底层逻辑开学第一周的电子技术实验课上&#xff0c;教室里突然响起此起彼伏的抱怨&#xff1a;“老师&#xff0c;我的Multisim打不开元件库&#xff01;”“提示说‘无法访问数据库’&#xf…

作者头像 李华
网站建设 2026/2/28 16:33:25

Sunshine硬件编码实战指南:解锁低延迟游戏串流的终极秘籍

Sunshine硬件编码实战指南&#xff1a;解锁低延迟游戏串流的终极秘籍 【免费下载链接】Sunshine Sunshine: Sunshine是一个自托管的游戏流媒体服务器&#xff0c;支持通过Moonlight在各种设备上进行低延迟的游戏串流。 项目地址: https://gitcode.com/GitHub_Trending/su/Sun…

作者头像 李华
网站建设 2026/2/24 21:25:19

Python金融数据终极方案:问财API完整指南与实战

Python金融数据终极方案&#xff1a;问财API完整指南与实战 【免费下载链接】pywencai 获取同花顺问财数据 项目地址: https://gitcode.com/gh_mirrors/py/pywencai 还在为繁琐的股票数据收集而苦恼吗&#xff1f;&#x1f914; 每天手动整理财务报表、跟踪股价变动&…

作者头像 李华
网站建设 2026/3/1 0:39:45

BGE-M3部署指南:Docker容器化最佳实践

BGE-M3部署指南&#xff1a;Docker容器化最佳实践 1. 引言 1.1 业务场景描述 在现代信息检索系统中&#xff0c;文本嵌入模型&#xff08;Text Embedding Model&#xff09;已成为构建语义搜索、推荐系统和问答服务的核心组件。随着多语言、多模态内容的快速增长&#xff0c…

作者头像 李华
网站建设 2026/2/27 5:58:53

通义千问3-Embedding-4B安全部署:生产环境配置最佳实践

通义千问3-Embedding-4B安全部署&#xff1a;生产环境配置最佳实践 1. 模型概述与核心价值 1.1 Qwen3-Embedding-4B&#xff1a;面向多语言长文本的高效向量化引擎 Qwen3-Embedding-4B 是阿里通义千问&#xff08;Qwen&#xff09;系列中专为「文本向量化」任务设计的 40 亿…

作者头像 李华