1. ARM64 体系结构概述:从 ARMv7 到 ARMv8 的演进
各位好,我是老周。做 ARM 底层开发十几年了,今天咱们聊聊 ARM64 的体系结构。说实话,当年从 ARMv7 切到 ARMv8 的时候,我第一反应是:这玩意儿变化也太大了。但用久了你会发现,这些变化其实都是被逼出来的——移动设备要性能,服务器要地址空间,IoT 要低功耗,ARM 不得不做出改变。
1.1 为什么要有 ARMv8?
ARMv7 时代,我们用的都是 32 位地址空间,最大 4GB。这在手机时代还凑合,但到了服务器和云计算场景,4GB 就是个笑话。我有个朋友做数据库加速卡,ARMv7 上折腾了半年,最后发现内存映射根本不够用——这就是硬伤。
ARMv8 带来了两个核心变化:
- 64 位地址空间:最大支持 2^48 字节虚拟地址,256TB,够你用一辈子
- 两种执行状态:AArch64(纯 64 位)和 AArch32(兼容 32 位)
说白了,ARMv8 不是简单地把寄存器从 32 位扩到 64 位,而是重新设计了一套指令集架构。嗯,这里要注意:AArch64 和 AArch32 是两种完全不同的执行状态,不是简单的"32 位模式"和"64 位模式"。
1.2 AArch64 vs AArch32:到底差在哪?
我刚开始做 ARM64 移植时,最头疼的就是这两个状态的区别。咱们直接看对比表:
| 特性 | AArch64 | AArch32 |
|---|---|---|
| 寄存器宽度 | 64 位通用寄存器 | 32 位通用寄存器 |
| 通用寄存器数量 | 31 个(X0-X30) | 15 个(R0-R14) |
| 指令长度 | 固定 32 位 | 可变(16/32 位 Thumb) |
| 异常等级 | EL0-EL3 四级 | PL0-PL2 三级 |
| 地址空间 | 48 位虚拟地址 | 32 位虚拟地址 |
| 条件执行 | 大部分指令取消条件执行 | 几乎所有指令可条件执行 |
| SIMD 支持 | NEON 标准集成 | NEON 可选 |
关键点:AArch64 不是 AArch32 的"64 位扩展版",而是全新的指令集。你没法直接把 AArch32 的代码重新编译成 64 位就跑起来——寄存器模型、异常模型、指令编码全变了。
为什么会这样?我个人的理解是:ARM 想借这个机会彻底甩掉历史包袱。ARMv7 时代为了兼容性,指令集里塞了太多"历史遗迹"。AArch64 是一次"clean slate"设计,该砍的砍,该加的加。
1.3 ARM64 寄存器模型详解
寄存器这块,我踩过不少坑。咱们一个一个说。
1.3.1 通用寄存器
AArch64 有 31 个 64 位通用寄存器,编号 X0 到 X30。每个 X 寄存器还可以通过 W 别名访问低 32 位:
// X0 是 64 位寄存器
// W0 是 X0 的低 32 位
MOV X0, #0x123456789ABCDEF // 存 64 位值
MOV W0, #0x12345678 // 只操作低 32 位,高 32 位清零
这里有个坑:写 W 寄存器会自动清零高 32 位。我当年第一次写 AArch64 汇编时,以为 W0 只是 X0 的低半部分,写 W0 不影响高 32 位——结果 debug 了一下午。记住:W 寄存器写入 = 隐式零扩展。
特殊用途寄存器:
- X30 (LR):链接寄存器,保存函数返回地址
- X29 (FP):帧指针,一般用于栈回溯
- SP:栈指针,注意 AArch64 里 SP 不是通用寄存器
- XZR:零寄存器,读总是 0,写被忽略
个人习惯:我写 ARM64 汇编时,习惯用 X0-X7 做参数传递,X8 做间接结果,X9-X15 做临时变量。这样看代码一目了然,也符合 ATPCS(ARM 过程调用标准)的约定。
1.3.2 系统寄存器
系统寄存器是 ARM64 的"控制面板"。你想想看,CPU 怎么知道当前在哪个异常等级?怎么配置 MMU?怎么处理中断?全靠系统寄存器。
常用的系统寄存器:
| 寄存器 | 功能 | 访问方式 |
|---|---|---|
| CurrentEL | 当前异常等级 | MRS X0, CurrentEL |
| SCTLR_EL1 | 系统控制寄存器 | MSR SCTLR_EL1, X0 |
| TCR_EL1 | 翻译控制寄存器 | MSR TCR_EL1, X0 |
| VBAR_EL1 | 异常向量表基地址 | MSR VBAR_EL1, X0 |
| DAIF | 中断屏蔽位 | MSR DAIFSet, #2 |
注意系统寄存器的命名规则:XXX_ELn,n 表示这个寄存器在哪个异常等级下可访问。比如 SCTLR_EL1 只能在 EL1 及以上访问,EL0 碰都碰不到。
1.3.3 异常等级与寄存器视图
ARM64 有四个异常等级:
- EL0:用户态,权限最低
- EL1:操作系统内核
- EL2:虚拟机监视器(Hypervisor)
- EL3:安全监视器(Secure Monitor)
每个异常等级都有自己的寄存器视图。比如 SP 寄存器,EL0 用 SP_EL0,EL1 用 SP_EL1,互不干扰。我曾经在调试一个虚拟化问题时,发现 EL2 代码改了 SP,结果 EL1 的栈指针也跟着变了——后来才发现是 SP 选择寄存器(SPSel)配置错了。
避坑指南:我曾经在 EL3 固件里误操作了 EL1 的系统寄存器,导致 Linux 内核启动时直接挂掉。调试了三天才发现是 SCTLR_EL1 被意外修改了。记住:系统寄存器的访问权限是硬性的,低等级不能访问高等级的寄存器,但高等级可以访问低等级的——要小心!
1.4 ARM64 体系结构全景图
下面这张图是我自己总结的 ARM64 体系结构全景,涵盖了从 ARMv7 到 ARMv8 的核心变化:
1.5 实际项目中的经验
最后分享几个我在项目中遇到的真实案例:
案例 1:AArch32 兼容模式的坑
有个项目需要在 ARMv8 芯片上跑老的 ARMv7 系统。我们以为开个兼容模式就完事了,结果发现 AArch32 模式下性能比原生 ARMv7 芯片还差。后来查手册才知道,AArch32 模式下某些流水线优化是被禁用的。所以我的建议是:能用 AArch64 就别用 AArch32 兼容模式。
案例 2:寄存器别名带来的困惑
调试一个性能问题时,我发现同样的代码在 AArch64 上比 AArch32 慢了 15%。排查了半天,最后发现是编译器把 W 寄存器和 X 寄存器混用了——写 W 寄存器导致的高 32 位清零操作产生了额外的依赖。解决方案很简单:统一用 X 寄存器,只在需要 32 位操作时用 W。