简介这是一套以Armv8/Armv9处理器架构为核心的系统性学习资料适合嵌入式软件、BSP、芯片验证、安全架构等方向工程师以及希望从入门到精通ARM底层原理的软硬件开发者。资源以PDF格式文档呈现共1个文件压缩包约7.22MB。内容覆盖ARM基础、异常中断与GIC、Trustzone安全架构、虚拟化、Cache、MMU、电源管理、SOC架构等关键模块尤其对工程中极难排查的Cache一致性问题和异常中断机制展开了深入讲解。作者拥有12年手机、汽车及芯片安全开发经验课程强调“以安全为核心”的ARM架构学习路径并能帮助读者读懂1.5万多页官方手册、建立大系统软件视野。目前已有106人获取学习适合需要体系化补齐ARM底层知识与安全设计能力的工程师用来推动实际项目中的架构理解和问题定位。1. 学好ARM真的能让领导刮目相看Armv8/Armv9架构不是背寄存器手册写了几年C代码一遇到系统起不来、内存被踩、性能上不去就只能加打印碰运气这种状态我太熟了。直到某次线上问题一位平时话不多的同事只看了一眼异常现场直接说是MMU页表属性配错导致二十分钟修完。那一刻我才意识到系统学习Armv8/Armv9架构不是为了背下来几百个寄存器而是把异常模型、MMU、Cache、启动流程这些零散知识点串成一张随时能调用的地图。这篇笔记会把Armv8/Armv9架构从入门到精通这条路拆成三个阶段先立住指令集和异常等级接着在QEMU里动手验证最后把知识落到性能排查和系统启动这类真实任务上。适合看过一些ARM汇编但没系统学过架构、或者正被底层疑难杂症卡住的嵌入式与基础软件工程师。2. Armv8与Armv9核心差异先把异常模型、特权级和MMU立住2.1 为什么说Armv8是分水岭AArch64不是ARMv7的64位版很多人第一次接触Armv8时会有一个错误预期AArch64就是把ARMv7的寄存器从32位扩到64位。实际完全不是这样。AArch64是一套全新的指令集虽然名字里带着ARMv8-A但它没有沿用ARMv7的条件执行后缀、多寄存器Load/Storeldmia这类、也没有Push/Pop这种栈操作指令。AArch64为用户提供了31个64位通用寄存器x0到x30其中x30固定作为链接寄存器LR在函数调用时保存返回地址。W0到W30是对应寄存器的低32位别名写W寄存器会清零高32位。PC不再是通用寄存器之一不能直接赋值只能通过跳转指令修改。SP与通用寄存器分离在EL1及以上还有SP_ELx这样的独立栈指针选择。这种设计带来的直接收益是调用约定更清爽参数传递和现场恢复的路径比32位时代更少指令编码也规整得多。这里给新手一个精通的检验标准不看手册能写出一份能跑的最小启动汇编能看懂一次异常现场的寄存器组合。达到这个程度你看代码的视角会从这一行做了什么升维到CPU在这段代码里处于什么状态。2.2 Armv9补的课SVE2、MTE、RME分别在解决什么问题Armv9是Armv8的延续而非替代它保留了v8的异常模型和内存模型在其上增加了三块关键能力。SVE2是向量扩展面向AI、图像、编解码这类计算密集型场景它把向量长度从固定的128位放宽到可伸缩范围。MTE内存标签扩展用于检测内存越界和释放后使用这是一种硬件辅助的内存安全方案对定位堆内存问题很有价值。RME机密计算扩展支撑机密计算场景把敏感负载放进受硬件保护的环境里执行。对大多数系统软件工程师来说最先能实际用上的很可能是MTE。它给每16字节内存打上标签指针的高位也存储标签读写时硬件校验标签是否一致。开启之后越界访问会在发生地直接触发异常而不是等到内存被破坏后再表现出诡异症状。SVE2目前主要价值在SIMD性能优化相关岗位RME则更多面向安全方向。特性解决的问题对系统工程师的直接价值SVE2向量宽度固定导致移植成本高高性能计算、AI算子库适配MTE内存越界/释放后使用难定位堆破坏问题直接硬件报异常RME高特权软件不可信机密计算、安全启动场景2.3 异常等级EL0到EL3决定了你能碰什么Armv8/Armv9把运行特权分为四个等级简称EL0到EL3。EL0跑用户态应用EL1跑操作系统内核EL2是虚拟化层EL3是安全固件。每个等级能访问的系统寄存器、对MMU的配置权限、可执行的特权指令都不同。高等级软件可以控制低等级反之不行。异常发生时CPU会自动把关键现场保存到当前异常等级对应的系统寄存器里SPSR_ELx保存PSTATEELR_ELx保存返回地址ESR_ELx记录异常原因。软件处理完异常后通过eret指令返回CPU会同时把特权级切回原等级。这套机制是理解中断、系统调用、缺页异常、调试器工作原理的共同底座。可以用一条指令直接观察当前运行在哪个等级// 读取当前异常等级 // 返回值低几位代表0x0 - EL00x4 - EL1 // 0x8 - EL20xc - EL3 mrs x0, CurrentEL这条mrs指令的效果是把CurrentEL系统寄存器读到X0里。在QEMU里跑裸机程序时根据机器配置不同你可能会在X0里看到0x4或0x8。关键在于如果你连程序当前在哪个异常等级都不确认后面所有对特权寄存器、MMU配置的排查都是在瞎试。这是ARM体系里第一个必须形成条件反射的问题。2.4 MMU与Cache系统工程师翻车最多的地方MMU负责虚拟地址到物理地址的翻译在AArch64里通过系统寄存器TCR_ELx、TTBR0_ELx、TTBR1_ELx、MAIR_ELx和翻译表描述符配合工作。TCR_ELx里的IPS字段决定物理地址宽度TG0决定页粒度大小SH0、ORGN0、IRGN0控制Cache属性和共享属性。MAIR_ELx是内存属性指示器用8位描述一个属性条目例如0x44表示Inner/Outer Write-Back。我见过太多人在刚接触MMU时把页表属性配成Device类型后直接跑代码结果一访问数据总线就异常也有人把Cache属性设成不可缓存性能断崖式下降却不知道去哪找原因。AArch64里普通内存用Normal属性外设寄存器必须用Device属性两者不能混用。这种细节在段式页表和绝大多数单片机裸机开发里都不会遇到但它是Armv8/Armv9系统软件必过的坎。学习建议是先不急着背字段位域而是在QEMU里把MMU关掉跑通一段汇编再逐步打开。每次只改一个配置项观察程序是崩溃、变慢还是正常。这种方式比对着手册空想要有效得多。后面的章节我会给出在QEMU里从零验证这些概念的完整流程。3. 在QEMU上把Armv8/Armv9跑起来最小汇编、反汇编与GDB单步3.1 三分钟跑通第一个AArch64程序用户态QEMU最小命令学习ARM架构最忌讳只看不跑。不用买开发板QEMU就够用。先搭一个最小的AArch64用户态实验环境验证基本指令和系统调用行为。安装工具链和模拟器# Ubuntu/Debian 系列 sudo apt install gcc-aarch64-linux-gnu qemu-user qemu-system-arm写一段不依赖任何库的汇编程序直接调用Linux系统调用输出字符串// start.S .global _start _start: mov x0, #1 // fd 1标准输出 ldr x1, msg // 缓冲区地址 mov x2, #10 // 长度Hello ARM\n 正好 10 字节 mov x8, #64 // AArch64 Linux write 系统调用号 svc #0 // 触发系统调用陷入 EL1 mov x0, #0 // 返回值 0 mov x8, #93 // exit 系统调用号 svc #0 msg: .ascii Hello ARM\n编译并运行aarch64-linux-gnu-gcc -static -nostdlib -Wl,-Ttext0x400000 start.S -o hello qemu-aarch64 ./hello这段实验说明了一个核心概念应用层代码运行在EL0当执行svc #0时CPU触发一个同步异常切入EL1的内核态由操作系统根据X8里存放的系统调用号执行相应服务。write在AArch64 Linux里的编号是64exit是93这是极少数需要硬记的常量。整个流程就是ARM异常模型的最小化演示理解了它再看中断和陷入类异常就会顺很多。3.2 用QEMU system模式观察启动现场链接脚本、CurrentEL与串口用户态模拟只能看到指令执行结果看不到系统启动过程。想理解复位、异常向量、特权级切换这些内容需要用qemu-system-aarch64跑完整系统模拟。先做一份最简链接脚本把代码放在QEMU virt平台默认的RAM起始地址# arm64.lds SECTIONS { . 0x40000000; .text : { *(.text) } }再写一段启动代码读取当前异常等级后进入死循环// boot.S .section .text .global _start _start: mrs x0, CurrentEL // 读取当前异常等级 // 0x4 - EL10x8 - EL20xc - EL3 1: wfe // 等待事件降低功耗 b 1b // 死循环便于观察 X0编译运行aarch64-linux-gnu-gcc -nostdlib -Wl,-T,arm64.lds boot.S -o boot.elf qemu-system-aarch64 -machine virt -cpu cortex-a53 -kernel boot.elf -nographic -S -s这里-machine virt表示使用QEMU提供的虚拟开发板-cpu cortex-a53指定一个Armv8-A处理器模型-kernel boot.elf把程序当作内核镜像加载到RAM起始地址-nographic把串口绑定到当前终端-S让CPU启动后立即暂停-s在1234端口开放GDB调试服务。然后在另一个终端连接GDBaarch64-linux-gnu-gdb boot.elftarget remote :1234 info registers x0你会看到X0寄存器里存放的异常等级值。不同QEMU版本和machine配置下可能是0x4或0x8这没关系重点是理解QEMU启动裸机程序时已经处在高特权级而真实设备的启动过程通常是从EL3开始逐级往下跳。这个实验能直观回答CPU复位后到底在哪的问题。3.3 反汇编与GDB单步让指令执行过程变得可见理解指令编码是深入学习的基础。用一段最简单的加法指令看它的机器码cat inst.s EOF add x0, x1, x2 EOF aarch64-linux-gnu-as inst.s -o inst.o aarch64-linux-gnu-objdump -d inst.o输出会显示这条指令编码为8b020000。AArch64固定指令长度是32位add这类寄存器指令的编码布局包含操作码、目标寄存器号、两个源寄存器号和移位字段。手算一次编码你对指令集的感觉会跟只看手册完全不同。8b020000里的最后5位00000对应X0往前5位00010对应X2再往前5位00001对应X1这种位域规整性是AArch64在设计上刻意追求的。GDB单步调试是排查系统问题的基本功。在前面启动实验的基础上让CPU暂停在每个指令边界aarch64-linux-gnu-gdb boot.elftarget remote :1234 info registers pc sp x0 si info registers pc x0si是单步执行一条机器指令info registers查看当前寄存器现场。刚开始学架构时用这种方式盯住PC的变化能清楚看到每个跳转目标、每次异常返回路径比在纸上推演直观得多。这套流程同样是以后排查真实启动问题的标准动作形成肌肉记忆后收益很大。4. ARM架构自学避坑指南五类典型翻车现场与排查路径4.1 上电就死循环链接地址与入口符号对不上现象在QEMU里加载自己写的boot.elf后GDB显示PC停在0x0或者某个随机地址程序完全没有执行到自己的代码。加上打印也无济于事。原因最常见的是链接脚本里的起始地址和CPU实际取指地址不一致。QEMU virt平台把内核镜像加载到0x40000000但如果你链接到了0x80000000PC就会指向全零的内存区域另一个常见原因是入口点没指定链接器默认从.text开头执行但.text段里第一个符号未必是你期望的_start。解决链接脚本里明确. 0x40000000;并在编译命令中加-Wl,-e,_start强制入口点。启动早期还要初始化栈指针在进入C代码前至少执行msr spsel, #1 // 选EL1的栈指针 ldr x0, stack_top mov sp, x0检查顺序是先看PC在不在0x40000000附近再看SP有没有值最后确认入口符号是否匹配。4.2 现场被洗掉LR被覆盖后该怎么定位现象写了一个带函数调用的程序函数里又调了别的函数结果返回时PC跳到垃圾地址程序崩掉。GDB里查看X30发现值已经面目全非。原因在AArch64里bl指令把返回地址写入X30但如果内层函数再次执行bl新的返回地址会覆盖掉外层保存的值。这是寄存器调用约定最基础的规则X30是调用方保存寄存器每个函数在入口处必须先把它压栈。解决函数入口和返回要配对保存、恢复func: stp x29, x30, [sp, #-16]! // 压栈保存帧指针和LR ... ldp x29, x30, [sp], #16 // 弹栈恢复 ret如果是排查异常现场反而不能依赖X30。异常发生时CPU已经把现场保存到了ELR_ELx里ELR_ELx保存的是触发异常的那条指令的地址SPSR_ELx保存当时的处理器状态ESR_ELx里是异常原因编码。查这类问题要看这三个系统寄存器业务日志反而没那么快。4.3 打开MMU就重启页表和内存属性踩坑现象按网上教程配置好页表执行打开MMU的那条指令后系统立即进入异常或挂死。关掉MMU一切正常。原因最典型的是内存属性配置错误。把正常内存标成了Device属性或者MAIR_EL1里属性索引对应的位域没写对另一种常见原因是TCR_EL1里的IPS字段比实际物理地址宽度小翻译表访问越界。解决先确认MAIR配置一个最小可用的配置方式是给两个属性槽// MAIR_EL1Attr0 Normal WBAttr1 Device-nGnRnE // 0x44 表示 Outer Write-Back、Inner Write-Back ldr x0, 0x0000000000000044 msr mair_el1, x0再检查TCR_EL1的IPS、TG0、SH0、ORGN0、IRGN0字段。排查时把MMU配置拆成两步先配置但不打开读回验证每个系统寄存器值确认无误再置位SCTLR_EL1的M位。不要用C语言驱动这段代码在MMU打开前后运行环境会发生剧变最好用汇编写完再跳到C入口。4.4 控制台没输出串口地址与模拟器模型不匹配现象按某块开发板的UART地址往寄存器里写字符QEMU里什么也不打印。原因不同平台、不同开发板的串口地址完全不同。QEMU virt机器使用PL011串口地址固定在0x09000000而很多教程基于树莓派或其它开发板写的地址可能是0x3F200000或0x10000000直接套用必然没输出。解决确认当前模拟器对应的设备地址最可靠的方式是看QEMU导出的设备树qemu-system-aarch64 -machine virt,dumpdtbvirt.dtb dtc virt.dtb -o virt.dts grep -A 5 uart virt.dts设备树里会列出串口节点和它的reg属性地址。养成先确认硬件模型再写驱动代码的习惯会帮你避开大量无意义的调试时间。4.5 老教程的32位习惯AArch64里不能用的老写法现象照着多年以前的ARM教程写汇编ldmia sp!, {r0-r3}、push {r4, lr}这类指令在交叉编译时直接报语法错误。原因AArch64不是ARMv7寄存器的简单加宽而是一套独立的指令集。32位时代的条件执行后缀、多寄存器加载存储、ldr r0, [r1], #4这类形式在AArch64里都不存在。ARMv8保留了AArch32执行状态但那是在同一个CPU里切换到另一套模式不是AArch64的兼容子集。解决学AArch64时把老的32位习惯全部放下。写代码前先明确自己用的是-marcharmv8-a还是更低的架构版本编译器选项里也不要混用。你会很快发现AArch64的指令编码更规整、寻址方式更简洁反而比老指令集好记。5. 把ARM知识变成能复用的能力一条排障链路和两个验证技巧5.1 把知识固化成一条启动排障链路我习惯把散落的架构知识收拢成一张表每次调试系统启动问题都按这个顺序走一遍步骤检查内容出问题时的典型现象1PC是否在链接地址处跑到0x0或随机地址2SP是否初始化第一次函数调用就崩3当前异常等级系统寄存器访问报错4异常向量表地址中断/异常进入死循环5MMU页表和内存属性开MMU即挂6串口地址与时钟控制台无输出这套链路是从无数次翻车里提炼出来的。先确认运行环境再查配置最后才怀疑业务逻辑顺序反了就是在浪费时间。5.2 用PMU做简单的内存行为验证ARMv8提供了性能监视单元PMU可以用来验证一个具体的内存访问是命中了Cache还是走到了主存。在EL0访问PMU计数器需要先让EL1使能PMUSERENR_EL0// 使能EL0访问PMU寄存器 mrs x0, pmuserenr_el0 orr x0, x0, #1 msr pmuserenr_el0, x0之后在代码里读周期计数寄存器PMCCNTR_EL0对比一段数据操作前后的计数差值就能粗略判断这段代码的访存效率。这个方法虽然不能替代专业profiler但用来验证自己写的代码是否符合对Cache行为的预期已经足够有说服力。有一回我调试一个看似随机的死锁查了两天没头绪最后安静下来先读了CurrentEL发现程序比预期低了一个异常等级整个排查方向从一开始就是错的。从那以后我要求自己定位系统问题先确认EL再看SPSR、ELR、ESR最后才是业务日志。这个习惯帮我省下过无数个加班的夜晚希望也能帮到你。本文还有配套的精品资源点击获取