ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

用QEMU调试Cortex-M33 MPU:从配置到MemManage Fault的完整实验

用QEMU调试Cortex-M33 MPU:从配置到MemManage Fault的完整实验 说实话最开始我并不太热衷于用QEMU去调MPU。原因很简单QEMU毕竟是个模拟器跑个裸机点灯、验证外设驱动还行MPU这种跟“异常”、“访问控制”强相关的东西模拟器能复现出真机那种微妙的行为吗直到我接手一个Cortex-M33项目厂家板子的调试器固件版本老旧每次配置MPU错误都得重新烧录、连调试器读寄存器、再翻手册比对寄存器位一个上午就搭进去三回。后来我索性把整套实验搬到了QEMU上在mps2-an505这台虚拟的MPS2开发板环境里跑Cortex-M33从配置MPU到触发MemManage Fault再到恢复现场全部可以在主机上直接观察。这个“用QEMU玩转Cortex-M33 MPU”的方法对刚开始接触ARMv8-M、TrustZone以及MPU的开发者来说是一条成本极低的学习路径。这篇文章不打算讲太虚的概念直接给你一整套能编译、能运行、能看到fault现场的实验工程。你不需要买板子也不需要J-Link只需要一台能跑QEMU的x86机器装上交叉编译工具链照着我下面的步骤走一遍就能亲手感受到MPU是怎么把一个“越权访问”拦下来的也能学会用QEMU的调试手段去定位这类问题。适合正在做Cortex-M33相关开发、或者想搞懂ARMv8-M内存保护机制的朋友。1. 为什么我坚持认为MPU调试应该先在QEMU上做1.1 真机调试MPU的三个痛点真机上调MPU最烦的不是配置本身而是出错之后的排查效率。MPU的配置一旦把关键内存区域的访问权限收得过紧CPU立刻就会进入异常连printf都可能来不及输出程序直接卡死。这个时候你只能靠调试器去读SCB-CFSR、SCB-MMFAR这些寄存器才能知道是哪个地址访问违例了。听起来不难问题是很多开发板的调试器连接不稳定加上Cortex-M33大多配合TrustZone使用调试器的连接方式比传统Cortex-M复杂现场一乱排查时间成倍增长。另外一个痛点是板子的外设地址各不相同。你在A厂家的芯片上调好的MPU region换到B厂家的板子外设基地址可能完全不同链接脚本、内存布局都要跟着改。而QEMU的MPS2平台是固定的内存地图清清楚楚学一次原理跑到哪里都能用。最让我崩溃的是烧录等待。真机上每改一次代码编译、烧录、复位、复现异常、抓现场一个循环怎么也得一两分钟如果是复杂一点的配置一天下来大部分时间都耗在等烧录器上了。QEMU秒级启动改完直接跑效率完全不在一个量级。1.2 QEMU能替你做哪些事不能替你做哪些事QEMU在MPU调试上的强项首先是异常现场可视化。MemManage Fault发生时QEMU的日志模式可以直接把寄存器和内存状态打出来甚至能用GDB断点停在handler入口像看普通程序一样看异常现场。其次是随意构造非法访问。在真机上你不敢随便写一个越权访问的测试代码因为真机可能因为配置错误导致锁死但QEMU里你随便作大不了重启虚拟机成本几乎为零。但QEMU也有明显的边界。它不会模拟真实的内存时序也不会模拟CacheCortex-M33本身没有内部Cache但部分带Cache扩展的型号有所以你在QEMU里感受到的MPU性能开销和真机并不一致。此外QEMU对一些外设的安全属性建模并不完整特别是TrustZone中涉及物理隔离的部分QEMU能做到逻辑层面的模拟但没法覆盖所有硬件细节。我的建议是逻辑和权限设计先在QEMU里验证时序、低功耗、外设电气特性这些仍然需要真机。1.3 什么水平的读者适合直接上手这篇文章适合三类人一是刚开始用Cortex-M33做开发想理解MPU到底怎么配置才安全的嵌入式工程师二是学习ARMv8-M架构想通过实验验证理论的学生三是做RTOS移植或者安全启动方案的开发者手里有真机但不想频繁烧录的。你只需要会基本的C语言和Makefile知道什么是编译、链接就能跟上。2. 环境准备QEMU版本、交叉编译链与MPS2机型选择2.1 安装QEMU注意别用太老的版本QEMU对MPS2平台的支持已经有很多年了但不同版本的完成度差别很大。早期的QEMU对Cortex-M33的MPU建模不完整跑起来经常出现匪夷所思的行为。我建议直接使用当前最新的稳定版比如QEMU 8.x或9.x尤其是9.0以后的版本mps2-an505和mps2-an521这两个机器的支持已经相当成熟。在Ubuntu或Debian上可以直接用apt安装sudo apt update sudo apt install qemu-system-arm装完之后验证一下版本并确认MPS2机器列表是否存在qemu-system-arm --version qemu-system-arm -machine help | grep mps2如果machine help里能看到mps2-an385、mps2-an500、mps2-an505、mps2-an521那环境就基本可用了。注意qemu-system-arm和qemu-system-aarch64是两个不同的二进制包本文用到的Cortex-M33是32位ARM架构需要的是qemu-system-arm别下错了。2.2 安装arm-none-eabi-gcc交叉编译器裸机程序编译需要arm-none-eabi工具链。Ubuntu下同样可以直接安装sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi验证arm-none-eabi-gcc --version如果你的发行版源里的工具链版本太老建议去Arm官网下载最新的GNU Arm Embedded Toolchain解压后把bin目录加到PATH里。这里没什么技术含量但版本老确实可能遇到奇怪的问题比如某些新指令不支持我遇到过gcc 9的链接器对ARMv8-M的某些section处理不干净换新版本就好了。2.3 为什么选mps2-an505而不是其他MPS2型号QEMU里MPS2平台有多个型号mps2-an385是Cortex-M3mps2-an500是Cortex-M7mps2-an505是单核Cortex-M33mps2-an521是双核Cortex-M33。既然目标是学习Cortex-M33的MPU选AN505最合适单核结构简单而且它使用了SSE-200子系统地址映射清晰适合教学实验。另外AN505的默认启动方式也很有意思。它把向量表从地址0x00000000开始加载代码和RAM分别映射在ITCM和DTCM上非常像一个“干净的MCU模型”比直接在STM32L5这种芯片上做实验更容易理解。2.4 先用QEMU自带的方式验证环境通不通环境是否通了不一定要先写程序。QEMU的-kernel参数可以直接加载一个裸机二进制文件到内存开头。我们可以先用一个最简单的、只有向量表和空循环的bin来测试避免一上来就扎进MPU配置里。一个空程序如果能在QEMU里正常启动不报错说明机器、工具链、启动方式都对了。3. MPS2 AN505内存布局与最小启动工程3.1 内存地图ITCM、DTCM和外设区域MPS2 AN505的内存分布是QEMU模拟出来的固定地址学习它比翻一摞芯片手册来得舒服。关键地址如下地址范围用途0x00000000 - 0x0007FFFFITCM代码区中断向量表在这里0x20000000 - 0x2003FFFFDTCMRAM区栈和全局变量在这里0x40000000 - 0x4FFFFFFFSSE-200内部外设、FPGAIO等0xE0000000 - 0xE00FFFFFUART、定时器、系统控制等外设0xF0000000 附近系统控制寄存器、SCC需要特别注意的是Cortex-M33的内核外设SCB、NVIC、MPU寄存器映射在0xE000E000开始的系统控制空间这部分和普通的MCU外设不一样是ARM内核定义的必须清楚。3.2 极简链接脚本把代码放ITCM数据放DTCM有了内存地图链接脚本就很好写了。下面是我用的mps2_an505.ldENTRY(Reset_Handler) MEMORY { FLASH (rx) : ORIGIN 0x00000000, LENGTH 512K RAM (rwx) : ORIGIN 0x20000000, LENGTH 256K } _estack ORIGIN(RAM) LENGTH(RAM); SECTIONS { .text : { KEEP(*(.isr_vector)) *(.text*) *(.rodata*) . ALIGN(4); _etext .; } FLASH .data : AT(_etext) { _sdata .; *(.data*) . ALIGN(4); _edata .; } RAM .bss : { _sbss .; *(.bss*) *(COMMON) . ALIGN(4); _ebss .; } RAM }这个脚本里_estack直接用RAM的最高地址作为栈顶简单可靠。_sdata、_edata、_etext是启动代码拷贝data段用的标号后面汇编里会用到。3.3 启动代码和向量表启动代码的逻辑很清楚硬件复位后从0x00000000取出初始栈指针从0x00000004取出Reset_Handler地址然后跳到Reset_Handler执行。Reset_Handler里做的事情只有三件把.data段从Flash拷到RAM把.bss段清零最后调用main。.syntax unified .cpu cortex-m33 .thumb .section .isr_vector,a,%progbits .align 2 .globl __Vectors __Vectors: .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler .word MemManage_Handler .word BusFault_Handler .word UsageFault_Handler .word 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 .word 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 .word 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 .section .text .thumb_func .globl Reset_Handler Reset_Handler: ldr r0, _sdata ldr r1, _edata ldr r2, _etext b 2f 1: ldr r3, [r2], #4 str r3, [r0], #4 2: cmp r0, r1 bcc 1b ldr r0, _sbss ldr r1, _ebss movs r2, #0 b 4f 3: str r2, [r0], #4 4: cmp r0, r1 bcc 3b bl main b . .thumb_func .weak NMI_Handler NMI_Handler: b NMI_Handler .thumb_func .weak HardFault_Handler HardFault_Handler: b HardFault_Handler .thumb_func .weak MemManage_Handler MemManage_Handler: b MemManage_Handler .thumb_func .weak BusFault_Handler BusFault_Handler: b BusFault_Handler .thumb_func .weak UsageFault_Handler UsageFault_Handler: b UsageFault_Handler这里把几个fault handler定义成weak符号后面C代码里如果定义了强符号链接器会优先使用C的版本。很多人第一次写M33启动代码会忘记.thumb_func结果跳转时处理器状态切到ARM模式立刻HardFault。这是个老坑。3.4 Makefile和第一条输出用semihosting的方式在QEMU里打一条字符串可以绕开串口驱动的复杂度便于先验证工程本身没问题。Makefile是这样的CC arm-none-eabi-gcc OBJCOPY arm-none-eabi-objcopy CFLAGS -mcpucortex-m33 -mthumb -O2 -g -Wall -Wextra -ffreestanding -nostdlib LDFLAGS -mcpucortex-m33 -mthumb -nostdlib -Wl,--gc-sections -T mps2_an505.ld OBJS startup_mps2.o main.o all: mpu_demo.elf mpu_demo.bin mpu_demo.elf: $(OBJS) $(CC) $(LDFLAGS) -o $ $(OBJS) %.o: %.c $(CC) $(CFLAGS) -c -o $ $ %.o: %.s $(CC) $(CFLAGS) -x assembler-with-cpp -c -o $ $ mpu_demo.bin: mpu_demo.elf $(OBJCOPY) -O binary $ $ clean: rm -f *.o *.elf *.bin运行命令qemu-system-arm -machine mps2-an505 -cpu cortex-m33 \ -nographic -kernel mpu_demo.bin -semihosting如果能看到你打印的字符串说明整条链路已经通了。4. Cortex-M33的MPU到底改了什么与ARMv7-M对比理解4.1 从ARMv7-M到ARMv8-MMPU最大的变化是“Region拆成了两部分寄存器”在Cortex-M3/M4上MPU的每个region通过一个RBAR寄存器基地址大小权限加上子区域掩码来配置。region的基地址、大小、访问权限全挤在一个寄存器里配置起来虽然也不算难但确实绕。Cortex-M33采用的ARMv8-M MPU把region定义拆成了两个寄存器RBAR负责描述基地址和访问权限AP、XN、SHRLAR负责描述limit地址和属性索引AttrIdx。属性类型又单独放在MAIR0/MAIR1寄存器里通过AttrIdx索引。这种设计让“地址范围”和“内存属性”两个维度彻底解耦一个MAIR属性可以被多个region复用逻辑上更符合现代MMU的设计思路。4.2 一个Region由哪三个寄存器决定MPU_MAIR0和MPU_MAIR1提供了8个8位的属性槽位每个槽位可以编码Device或Normal内存类型以及对应的Cache策略。比如0x44表示Normal内存non-cacheable0x00表示Device-nGnRnE0xFF表示Normal内存Write-Back Write-Allocate。这些属性值不需要死记知道怎么查就行。RBAR除了基地址还包含APAccess Permission和XNExecute Never字段。AP决定了该区域在特权模式和用户模式下的读写权限XN则控制是否允许取指。RLAR的低位放着AttrIdx和ENEN是这个region的使能开关。一个region在实际生效前必须同时满足RBAR设置了正确的基地址RLAR设置了正确的limit和AttrIdx且EN为1。4.3 AP和Attr到底谁管什么容易混淆我见过不少人在这个地方搞混AP管的是“谁能访问”Attr管的是“访问时的行为”。AP是权限控制比如特权模式可读写、用户模式不可访问Attr是内存类型控制比如这个区域是Device还是NormalCache策略是什么。两者不互相替代。理解一个典型场景就清楚了外设寄存器区域通常配成Device内存同时AP应该是特权模式可读写、用户模式不可访问。而普通的RAM区域可以配成Normal内存AP设为用户可读写。QEMU里也是这样把区域访问权限收得再狠也不会影响Memory Type属性。4.4 QEMU里的MPU行为跟真机一样吗QEMU对ARMv8-M MPU的建模已经相当完整尤其在你触发MemManage Fault的时候CFSR、MMFAR这些寄存器的行为和真机的软件模型是吻合的。但它毕竟是模拟器不会精确模拟Cache的时序效应也不会模拟某些芯片厂商额外加入的安全扩展。所以用QEMU验证“权限规则”效果很好用QEMU验证“性能影响”效果为零。5. 手写MPU实验用户态访问特权Region的完整过程5.1 实验设计思路实验不能太做作要真实可感。我的设计是这样的在DTCM里划出两块region一块0x20000000-0x2001FFFF设为特权模式专用另一块0x20020000-0x2003FFFF设为用户模式可访问。然后程序先在特权模式下读0x20000000再切到用户模式读同一个地址触发MemManage Fault。异常处理程序里采集CFSR、MMFAR等信息再把特权region的权限放开返回用户模式后再次读取这次就能成功。在这里还要特别强调一个重要的细节不能把整个Flash设为特权专用。因为用户模式下的代码取指仍然在Flash区域如果把Flash的AP设成特权专用CPU在用户模式下连取指都会被拦截根本走不到你设计好的“越权访问”那一步。实验设计阶段一定要想清楚“数据访问”和“取指访问”的区别。5.2 完整的MPU配置代码下面这段代码是一个可直接运行的main.c去掉了多余的头文件依赖只用寄存器宏实现#include stdint.h /* SCB */ #define SCB_SHCSR (*(volatile uint32_t *)0xE000ED24UL) #define SCB_CFSR (*(volatile uint32_t *)0xE000ED28UL) #define SCB_MMFAR (*(volatile uint32_t *)0xE000ED34UL) /* MPU registers */ #define MPU_CTRL (*(volatile uint32_t *)0xE000ED94UL) #define MPU_RNR (*(volatile uint32_t *)0xE000ED98UL) #define MPU_RBAR (*(volatile uint32_t *)0xE000ED9CUL) #define MPU_RLAR (*(volatile uint32_t *)0xE000EDA0UL) #define MPU_MAIR0 (*(volatile uint32_t *)0xE000EDC0UL) #define MPU_MAIR1 (*(volatile uint32_t *)0xE000EDC4UL) /* RBAR/RLAR bit helpers, layout follows CMSIS core_cm33.h */ #define MPU_RBAR_XN_Pos 12UL #define MPU_RBAR_AP_Pos 8UL #define MPU_RBAR_SH_Pos 6UL #define MPU_RBAR_REGION_Pos 1UL #define MPU_RBAR_VALID_Pos 0UL #define MPU_RBAR_MAKE(xn, ap, sh, region, base) \ (((uint32_t)(xn) MPU_RBAR_XN_Pos) | \ ((uint32_t)(ap) MPU_RBAR_AP_Pos) | \ ((uint32_t)(sh) MPU_RBAR_SH_Pos) | \ ((uint32_t)(region) MPU_RBAR_REGION_Pos) | \ ((uint32_t)(base) 0xFFFFFFE0UL) | \ (1UL MPU_RBAR_VALID_Pos)) #define MPU_RLAR_MAKE(attr, limit_plus_1) \ (((uint32_t)(limit_plus_1) 0xFFFFFFE0UL) | \ ((uint32_t)(attr) 1UL) | 1UL) #define AP_FULL 3UL #define AP_PRIV 1UL #define ATTR_NORMAL_NC 0UL #define ATTR_NORMAL_WBWA 1UL #define ATTR_DEVICE_NGNRNE 2UL static volatile struct { uint32_t cfsr; uint32_t mmfar; uint32_t pc; uint32_t relaxed; } fault_sig; /* semihosting: write string to QEMU stdin/stdout */ static void semihost_write0(const char *s) { register uint32_t r0 __asm(r0) 0x04; register const char *r1 __asm(r1) s; __asm volatile(bkpt 0xab : : r(r0), r(r1) : memory); } static void print_str(const char *s) { semihost_write0(s); } static void print_hex32(uint32_t v) { static const char hex[] 0123456789ABCDEF; char buf[11]; buf[0] 0; buf[1] x; for (int i 9; i 2; i--) { buf[i] hex[v 0xF]; v 4; } buf[10] \0; semihost_write0(buf); } static void mpu_put_region(uint32_t idx, uint32_t base, uint32_t end, uint32_t attr_idx, uint32_t ap) { MPU_RNR idx; MPU_RBAR MPU_RBAR_MAKE(0, ap, 0, idx, base); MPU_RLAR MPU_RLAR_MAKE(attr_idx, end 1U); __asm volatile(dsb ::: memory); __asm volatile(isb); } static void mpu_init(void) { /* MAIR0: attr0Normal non-cacheable(0x44), attr1Normal WBWA(0xFF), attr2Device-nGnRnE(0x00) */ MPU_MAIR0 (0x44UL 0) | (0xFFUL 8) | (0x00UL 16); MPU_MAIR1 0UL; /* Region0: entire ITCM/Flash, full access */ mpu_put_region(0, 0x00000000U, 0x000FFFFFU, ATTR_NORMAL_WBWA, AP_FULL); /* Region1: upper DTCM, user accessible */ mpu_put_region(1, 0x20020000U, 0x2003FFFFU, ATTR_NORMAL_NC, AP_FULL); /* Region2: lower DTCM, privileged only */ mpu_put_region(2, 0x20000000U, 0x2001FFFFU, ATTR_NORMAL_NC, AP_PRIV); /* Region3: system control space, device memory */ mpu_put_region(3, 0xE0000000U, 0xE00FFFFFU, ATTR_DEVICE_NGNRNE, AP_FULL); /* ENABLE1, PRIVDEFENA1, HFNMIENA0 */ MPU_CTRL (1UL 0) | (1UL 1); __asm volatile(dsb ::: memory); __asm volatile(isb); } /* get PC from exception frame on MSP */ __attribute__((naked)) static uint32_t fault_pc_from_msp(void) { __asm volatile(mrs r0, msp\n\t ldr r0, [r0, #24]\n\t bx lr); } __attribute__((naked)) static void switch_to_unprivileged(void) { __asm volatile(mrs r0, control\n\t orr r0, r0, #1\n\t msr control, r0\n\t isb\n\t bx lr); } void MemManage_Handler(void) { fault_sig.cfsr SCB_CFSR; fault_sig.mmfar SCB_MMFAR; fault_sig.pc fault_pc_from_msp(); /* relax Region2 so that unprivileged access becomes legal */ mpu_put_region(2, 0x20000000U, 0x2001FFFFU, ATTR_NORMAL_NC, AP_FULL); fault_sig.relaxed 1; /* clear fault status: write 1 to clear */ SCB_CFSR SCB_CFSR; __asm volatile(dsb ::: memory); __asm volatile(isb); } int main(void) { print_str( CM33 MPU demo on QEMU MPS2-AN505 \r\n); mpu_init(); print_str([INIT] MPU regions configured\r\n); /* enable MemManage exception */ SCB_SHCSR | (1UL 16); print_str([INIT] MemManage exception enabled\r\n); /* write a magic value at the start of protected region, privileged OK */ *(volatile uint32_t *)0x20000000UL 0xC0FFEE00UL; uint32_t v *(volatile uint32_t *)0x20000000UL; print_str([PRIV] read 0x20000000 ); print_hex32(v); print_str(\r\n); switch_to_unprivileged(); print_str([USER] switched to unprivileged\r\n); /* this read should trigger MemManage fault */ v *(volatile uint32_t *)0x20000000UL; print_str([USER] read 0x20000000 again ); print_hex32(v); print_str(\r\n); print_str([USER] survived, fault_sig.cfsr ); print_hex32(fault_sig.cfsr); print_str(\r\n[USER] fault_sig.mmfar ); print_hex32(fault_sig.mmfar); print_str(\r\n[USER] fault_sig.pc ); print_hex32(fault_sig.pc); print_str(\r\n[USER] spin here\r\n); for (;;) { } }这里的MemManage_Handler被定义为强符号汇编里的weak符号会被它覆盖。handler里先保存现场再把Region2的权限从AP_PRIV临时改成AP_FULL相当于在异常处理中“救场”返回后程序还能继续跑。这是理解MPU动态更新能力的最直观实验。5.3 运行实验并观察输出编译运行make clean make qemu-system-arm -machine mps2-an505 -cpu cortex-m33 \ -nographic -kernel mpu_demo.bin -semihosting预期输出大致如下 CM33 MPU demo on QEMU MPS2-AN505 [INIT] MPU regions configured [INIT] MemManage exception enabled [PRIV] read 0x20000000 0xC0FFEE00 [USER] switched to unprivileged [USER] read 0x20000000 again 0xC0FFEE00 [USER] survived, fault_sig.cfsr 0x00000082 [USER] fault_sig.mmfar 0x20000000 [USER] fault_sig.pc 0x0000049A [USER] spin hereCFSR 0x00000082这行是关键。0x80表示MMARVALID说明MMFAR里的地址有效0x02表示DACCVIOL即数据访问违例。如果是取指访问违例你会看到0x01IACCVIOL。这个值能直接告诉你异常类型排查问题的时候非常有用。5.4 为什么CFSR和MMFAR能证明MPU生效MPU的实验最怕的就是“看起来正常但其实是没生效”。CFSR的值就是证据。在特权模式读0x20000000是成功的说明MPU没有阻止特权模式切到用户模式后立刻触发MemManage FaultCFSR里出现DACCVIOL说明MPU确实拦截了这次用户态数据访问。如果MPU没配置好或者没使能那么这次读操作会正常返回就不会看到fault。我建议你自己做个小改动试试把mpu_put_region(2, ...)里最后的AP_PRIV改成AP_FULL重新编译运行你会发现程序全程没有任何fault输出CFSR保持0MMFAR也不会变化。这就是对比验证的价值。6. QEMU调试MPU的几个实用技巧6.1 用QEMU monitor看内存和寄存器QEMU的monitor是一台隐藏的调试终端启动命令里加一行就可以打开qemu-system-arm -machine mps2-an505 -cpu cortex-m33 \ -nographic -kernel mpu_demo.bin -semihosting \ -monitor telnet:127.0.0.1:5555,server,nowait然后在另一个终端连接telnet 127.0.0.1 5555monitor里我最常用的命令有info registers查看当前CPU寄存器状态xp /16x 0x20000000以十六进制查看内存内容info mtree查看QEMU模拟出的内存树结构quit退出QEMU。比如你想确认MPU配置是否生效可以在程序spin的时候用info registers看CONTROL寄存器的值判断当前处理器处于特权模式还是用户模式。6.2 用GDB把断点直接设在MemManage_Handler里QEMU自带GDB stub裸机程序也能用GDB调试。启动时加两个参数qemu-system-arm -machine mps2-an505 -cpu cortex-m33 \ -nographic -kernel mpu_demo.bin -semihosting \ -gdb tcp::1234 -S-S表示启动后停在第一条指令之前等待GDB连接。然后打开一个终端用arm-none-eabi-gdb连接arm-none-eabi-gdb mpu_demo.elf (gdb) target remote :1234 (gdb) break MemManage_Handler (gdb) continue程序执行到MemManage_Handler时会自动停下。这时候你可以直接查看全局结构体(gdb) p fault_sig $1 {cfsr 0, mmfar 0, pc 0, relaxed 0}当你单步过触发fault的那一行后CFSR和MMFAR已经更新p fault_sig能看到完整的现场信息。这个调试方式比真机上打断点要清爽太多。6.3 让QEMU记录MPU相关日志QEMU还支持日志模式可以输出详细的异常和MMU访问记录。启动时加上qemu-system-arm -machine mps2-an505 -cpu cortex-m33 \ -nographic -kernel mpu_demo.bin -semihosting \ -d mmu,cpu -D /tmp/mpu_run.log运行完查看/tmp/mpu_run.log里面会记录CPU访问内存时触发的MPU fault细节。这个日志对于理解“哪条指令在哪个地址被拦”很有帮助。7. QEMU和真芯片的差异踩坑记录与选型建议7.1 QEMU不会暴露MPU配置带来的性能差异QEMU的内存访问时序是模拟的不考虑Cache miss、bus turnaround这些真实开销。你在QEMU里把整块RAM配成Device内存程序还是跑得飞快真机上如果这么配一条普通的读写指令可能要慢上几个数量级。因此用QEMU验证完逻辑后上真机前一定要把内存属性重新过一遍尤其是外设区域和热点数据区域。7.2 region对齐和limit地址的理解误区ARMv8-M MPU中region的基地址和limit地址都必须满足32字节对齐region大小必须是2的幂。我在代码里写mpu_put_region(2, 0x20000000U, 0x2001FFFFU, ...)传进去的end是闭区间的上界函数内部用end 1U作为RLAR的limit地址。如果你传进去的end不对齐RLAR的低5位会被强制清零配置出来的region范围和你想的完全不一样。QEMU里这种错误通常不会立刻报错但会以诡异的方式表现出来比如越界访问没有被拦截或者莫名触发fault。排查时先算对齐。7.3 关于TrustZone的边界情况Cortex-M33支持TrustZoneQEMU的mps2-an505也有一部分TrustZone相关建模但SAU的配置、安全/非安全属性检查、以及外设的安全隔离在不同QEMU版本上支持程度不一样。本文的实验是在默认的非安全增强特性之外做的刻意避开了TrustZone。如果你后续要研究SAU和Secure/Non-Secure切换建议单独用mps2-an521做并且在QEMU源码中确认该版本对TrustZone的支持范围。不要指望QEMU完全等价于真芯片的安全边界。7.4 什么样的项目适合先上QEMU我现在做Cortex-M33的项目凡是涉及“内存权限模型设计”、“启动早期配置”、“异常处理流程验证”的工作都先在QEMU的MPS2环境里跑通再移植到目标芯片。QEMU很适合这类场景因为它启动快、可重现、不烧调试器。只要你的目标芯片支持ARMv8-M的MPU模型那么基于这套环境的实验结论基本可以平滑迁移。反过来涉及低功耗、时钟、外设时序、DMA行为和实际中断延迟的项目QEMU不管用需要板级验证。我的习惯是先QEMU快跑权限逻辑再真机慢调硬件细节两条腿走路效率最高。最后再分享一个实际操作中养成的小习惯每次配置MPU前都先读一下MPU_TYPE寄存器确认当前实现到底支持多少个region。QEMU里这个值通常稳定但不同QEMU版本和不同芯片确实存在差异。拿到硬件先读这个寄存器能避免把region数量用超了导致后续莫名其妙的行为。希望这套QEMU MPS2 Cortex-M33 MPU的实验环境能帮你少走点坑。
返回列表