news 2026/9/27 1:15:27

QEMU仿真Cortex-M33:零硬件搭建MPU验证环境

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
QEMU仿真Cortex-M33:零硬件搭建MPU验证环境

搞嵌入式开发的兄弟应该都有这种体会——想认真研究Cortex-M33这种带TrustZone和MPU的新内核,第一步就很难迈:STM32L5、NXP LPC55S6这些板子少说几百块,还要配仿真器;更麻烦的是,MPU这类和内存权限相关的代码,一旦配错就是直接HardFault,真机上定位问题特别费劲。其实QEMU早就把ARM官方MPS2开发板模拟得很好了,尤其是mps2-an505和mps2-an521这两个Cortex-M33目标,CPU行为、NVIC中断控制器、SysTick、MPU这些内核关键路径都仿真得很到位,完全可以用来看懂ARMv8-M的MPU工作机制、验证裸机代码、调试权限异常,而且全程不需要一块实体板卡。

这篇文章我就带你从零搭一套QEMU + MPS2 AN505的仿真环境,不依赖任何开发板和调试器,手把手把启动代码、链接脚本、UART驱动、MPU初始化全部跑通,最后用一个“故意写只读内存”的例子把MemManage异常打出来。想搞懂ARMv8-M MPU的朋友、做TrustZone隔离验证的朋友、准备移植RTOS特别是要开MPU保护的朋友,都可以直接参考这套工程,或者说,直接抄作业。

1. 方案选型:为什么是QEMU + MPS2而不是一块真开发板

1.1 MPS2板卡在QEMU里的位置

QEMU对ARM Cortex-M系列MCU的支持,主要分两类:一类是模拟具体的芯片型号,比如某些STM32系列,外设往往只覆盖了最常见的UART、GPIO,很多时候一查文档会发现“这个型号支持不完整”;另一类是模拟ARM官方的评估/教学开发板,比如MPS2、Musca、Musca-B1,这些板卡的模拟成熟度普遍更高,因为QEMU社区长期拿它们做CI测试和功能验证。

MPS2全称是ARM MPS2+ FPGA Prototyping Board,本身是一块面向嵌入式设计和验证的FPGA板,板上有多个可切换的“AN”镜像,对应不同内核。QEMU里和Cortex-M33直接相关的有两个machine:

QEMU machine内核特点
mps2-an505Cortex-M33单核经典的M33教学/验证平台
mps2-an521Cortex-M33双核支持TrustZone、双核调试

这两个目标在QEMU里已经稳定存在很多年,用的都是ARMv8-M架构,非常适合学习带MPU、带TrustZone的现代Cortex-M处理器。相比之下,如果非要拿某个厂商的M33芯片模型来学MPU,外设没模拟全倒是小事,最怕的是QEMU版本更新之后machine名和内存映射变了,网上的教程对不上号,那才叫一个难受。MPS2系列就没有这个问题,社区资料相对多,行为也比较稳定。

1.2 这套仿真环境的边界在哪里

很多人会问,QEMU仿真出来的M33和真实芯片差多少?我的实测结论是:CPU内核层面——包括指令执行、异常模型、中断优先级、SysTick、MPU、TrustZone的SAU——非常接近真实行为,尤其是MPU这类由硬件直接检查的机制,QEMU会按照ARM架构手册的规则去模拟,该触发MemManage就触发MemManage,不会因为“仿真”就打折扣。

但外设层面就要降低预期了。QEMU的mps2-an505只实现了板载的部分外设,比如CMSDK UART、定时器、GPIO的一些关键寄存器,很多外设只是放了一个“unimplemented device”占位,读操作返回0、写操作直接忽略。如果你要做的项目重度依赖某个外设的细节时序,那还是得回到真机上去调。如果只是学MPU、学Cortex-M33编程模型、跑RTOS内核,这套仿真环境不仅够用,效率还比真机高——重启快、加打印方便、GDB随便打断点,根本不心疼。

怎么确认你自己的QEMU支持哪些machine?装好QEMU之后跑一条命令就行:

qemu-system-arm -machine help | grep mps2

能看到mps2-an505和mps2-an521,就说明这一步没问题。

2. Cortex-M33的MPU到底怎么配:RBAR、RLAR、MAIR一次讲清

2.1 ARMv8-M MPU和ARMv7-M MPU有什么不同

如果你之前玩过Cortex-M3或者M4,对ARMv7-M的MPU应该不陌生,它由RBAR和RASR两个寄存器定义一个区域:RBAR写基地址,RASR里塞了一大堆属性,包括Region大小、AP访问权限、TEX、Cacheable/Bufferable、XN不可执行等等。一个区域的所有属性都被揉在一个寄存器里,算SIZE字段的时候还得记住“2的(N+1)次方”这种规则,位操作一不小心就写错,改一个属性还可能误伤其他位。

到了Cortex-M33,也就是ARMv8-M架构,MPU的设计思路变了:区域仍然最多8个,但每个区域变成了RBAR + RLAR两个寄存器,内存属性被挪到了MAIR寄存器里统一管理。用我自己的话说,ARMv7-M是“每个人兜里揣一张写满信息的身份证”,ARMv8-M则是“身份证只写编号,具体权限去中央系统查”。这种改动的好处非常明显:内存属性的定义是全局复用的,多个区域可以引用同一个属性编号,不需要每个区域都重复写一遍缓存策略和权限位,代码上更好维护,也更容易让硬件做并行检查。

另一个容易被忽略的点是,ARMv8-M的MPU区域大小不再需要单独指定,而是由Base地址和Limit地址直接算出来的。在ARMv7-M时代你经常要算“64KB是Size=几”,到了ARMv8-M,你只需要保证Base和Limit都32字节对齐,区域范围自然就是两者之间。

2.2 RBAR、RLAR、MAIR三个寄存器的分工

先看RBAR,它的作用是指定区域的起始地址,同时携带一个“有效位”和“区域编号”。在32位模式下,RBAR的bit[31:5]是基地址,bit[4]是VALID,bit[3:0]是REGION。写入时如果VALID置1,MPU会把配置加载到当前MPU_RNR选中的区域,同时把区域编号锁存到REGION字段。

RLAR则是区域的结束地址,bit[31:5]是Limit地址,bit[3:1]是AttrIndx,也就是MAIR里的属性索引,bit[0]是ENABLE。一个区域要真正生效,必须RBAR和RLAR都写完,而且ENABLE必须是1。

MAIR寄存器负责存放内存属性,Cortex-M33有MAIR0和MAIR1两个寄存器,每个寄存器拆成8个字节,每个字节对应一个属性条目。MAIR里通常会编码内存类型(Normal、Device)、缓存策略(Write-Back、Write-Through、Non-cacheable)、共享属性,以及访问权限AP。这个设计很像查表:RBAR/RLAR只告诉MPU“这段地址是从哪到哪、用哪条属性”,具体属性长什么样,去MAIR里查。

用一个不太严谨但很好懂的例子类比,整个MPU就像小区停车场的管理系统:

  • MAIR:停车场的管理条例,比如“月租车可以停在A区”“访客车只能停在B区”。
  • RBAR:告诉你某个区域从哪个门开始。
  • RLAR:告诉你区域到哪个门结束。
  • MPU_RNR:当前正在配置哪个区域。

处理器每次访问内存时,硬件会把访问地址跟所有region的范围做比对。如果命中了某个region,就按那个region的属性决定“能不能读”“能不能写”“能不能取指”;如果没命中任何region,就靠MPU_CTRL里的PRIVDEFENA决定是允许访问还是立即触发MemManage。

2.3 配置MPU的标准流程

配置MPU的流程并不复杂,我建议按以下顺序操作,可以避免一半以上的“怪问题”:

  1. 先把MPU关掉,也就是MPU_CTRL的ENABLE位置0,防止配置过程中出现半生效状态。
  2. 配置MAIR0/MAIR1,把需要的内存属性写进属性表。
  3. 用MPU_RNR选中要配置的region编号。
  4. 写RBAR,设定基地址,VALID位置1。
  5. 写RLAR,设定Limit地址、属性索引,最后ENABLE位置1。
  6. 所有region配置完后,把MPU_CTRL打开,ENABLE位置1,按需打开PRIVDEFENA。

有一点必须特别提醒:RLAR的Limit地址必须32字节对齐,而且指定的是“允许访问的最大地址”。很多人从ARMv7-M转过来,习惯性地认为Limit传的区域结束地址,比如64KB区域就想传0x0000FFFF,结果发现上电就进Fault。正确的做法是传0x0000FFE0,因为0xFFFF不对齐,硬件不认。这个细节在我第一次写的时候也踩了,后面会专门讲到。

3. 从零搭建MPS2 AN505裸机工程:工具链、启动代码、串口Hello World

3.1 安装QEMU和ARM交叉工具链

环境我用的是Ubuntu 22.04,安装命令很简单:

sudo apt update sudo apt install -y qemu-system-arm gcc-arm-none-eabi gdb-multiarch

装完验证一下版本:

qemu-system-arm --version arm-none-eabi-gcc --version

Ubuntu仓库里的QEMU版本可能不是最新,但没关系,mps2-an505这个machine早就合入了主分支,6.2以上版本都能用。如果你非要体验最新QEMU,也可以去官网下源码编译,或者直接用发行版仓库里的新版包,不影响下面的操作。调试器方面,gdb-multiarch和arm-none-eabi-gdb都行,我后面示例用gdb-multiarch。

工程我建议建一个干净的目录,比如叫mps2-m33,里面放四个文件:

mps2-m33/ ├── Makefile ├── mps2.ld ├── startup.s └── main.c

后续所有代码都围绕这四个文件展开。

3.2 工程结构和链接脚本

MPS2 AN505在QEMU里的内存布局其实非常简单,上电后就是直接从SRAM执行代码,没有真正的Flash。AN505的SSRAM分为多个块,最常用的是从0x00000000开始的SRAM区,我这里规划如下:

  • FLASH区:0x00000000,长度0x10000,放向量表和代码,只读。
  • RAM区:0x00010000,长度0x30000,放数据、BSS、堆栈。

之所以把可执行代码放在0x00000000,是为了让中断向量表符合ARM Cortex-M的启动要求——向量表第一项是初始栈指针,第二项是Reset_Handler入口,M33会从这里开始取指。链接脚本这么写:

ENTRY(Reset_Handler) MEMORY { FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 0x10000 RAM (rwx) : ORIGIN = 0x00010000, LENGTH = 0x30000 } SECTIONS { .isr_vector : { KEEP(*(.isr_vector)) } > FLASH .text : { *(.text*) *(.rodata*) } > FLASH .data : { _sdata = .; *(.data*) _edata = .; } > RAM AT > FLASH _sidata = LOADADDR(.data); .bss : { _sbss = .; *(.bss*) *(COMMON) _ebss = .; } > RAM _estack = ORIGIN(RAM) + LENGTH(RAM); }

这里把.data段的加载地址放在FLASH,运行地址放在RAM,就需要启动代码在main之前把数据从Flash拷贝到RAM,同时把BSS段清零。这个动作没有编译器帮你做,必须自己写在启动文件里。

栈顶_estack我直接定在RAM末尾,也就是0x00040000。给一个完整的栈空间,后面的实验完全够用。

3.3 启动代码与UART驱动

startup.s里面要完成几件事:定义向量表、实现Reset_Handler、拷贝.data、清零.bss、跳转main,另外再补几个异常处理函数的弱定义。Cortex-M33属于ARMv8-M,向量表前几项是固定的:

.syntax unified .cpu cortex-m33 .thumb .section .isr_vector, "a" .align 2 .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler .word MemManage_Handler .word BusFault_Handler .word UsageFault_Handler .section .text .thumb_func .global Reset_Handler Reset_Handler: ldr r0, =_sdata ldr r1, =_edata ldr r2, =_sidata copy_loop: cmp r0, r1 bge zero_bss ldr r3, [r2], #4 str r3, [r0], #4 b copy_loop zero_bss: ldr r0, =_sbss ldr r1, =_ebss movs r2, #0 zloop: cmp r0, r1 bge go_main str r2, [r0], #4 b zloop go_main: bl main b . .thumb_func .weak NMI_Handler .weak HardFault_Handler .weak MemManage_Handler .weak BusFault_Handler .weak UsageFault_Handler .thumb_func NMI_Handler: HardFault_Handler: MemManage_Handler: BusFault_Handler: UsageFault_Handler: b .

这个启动文件的做法是裸机最标准的套路:先拷贝.data,再清零.bss,最后进入main。注意最后几个异常处理函数我用.weak声明,又在同一个文件里给了默认死循环实现,这样C代码里如果定义了同名的强符号,链接器就会用C文件里的那个,非常方便。

UART驱动部分,MPS2 AN505在QEMU里用的是CMSDK UART,基地址是0x40004000。这个UART比较简单,常用寄存器如下:

偏移寄存器作用
0x00DATA收发数据
0x04STATEbit0是TXBF发送忙标志,bit1是RXBF接收标志
0x08CTRLbit0使能TX,bit1使能RX
0x10BAUDDIV波特率分频值

QEMU里这颗UART的时钟是25MHz,计算波特率分频的公式是:

BAUDDIV = UARTCLK / (16 * baud)

如果要跑115200,就是:

25_000_000 / (16 * 115200) ≈ 13.56,取14

所以UART初始化只需要几条语句。发送字符时先轮询STATE的TXBF,等发送缓冲区空了再写DATA即可。

3.4 编译运行:先看到Hello World

main.c这一步先不碰MPU,只做UART初始化和一行打印,确保整个工具链、启动文件、链接脚本是正确的:

#include <stdint.h> #define UART0_BASE 0x40004000UL #define UART_DATA (*(volatile uint32_t *)(UART0_BASE + 0x00)) #define UART_STATE (*(volatile uint32_t *)(UART0_BASE + 0x04)) #define UART_CTRL (*(volatile uint32_t *)(UART0_BASE + 0x08)) #define UART_BAUDDIV (*(volatile uint32_t *)(UART0_BASE + 0x10)) static void uart_init(void) { UART_BAUDDIV = 14; UART_CTRL = 0x3; /* TX enable | RX enable */ } static void uart_putc(char c) { while (UART_STATE & 0x1) { } UART_DATA = c; } static void uart_puts(const char *s) { while (*s) { uart_putc(*s++); } } int main(void) { uart_init(); uart_puts("Hello from Cortex-M33 on QEMU MPS2-AN505\r\n"); while (1) { } }

Makefile我写的是最朴素的一版,方便你看清楚编译参数:

CROSS ?= arm-none-eabi- CC = $(CROSS)gcc LD = $(CROSS)gcc OBJCOPY = $(CROSS)objcopy CFLAGS = -mcpu=cortex-m33 -mthumb -Wall -Wextra -O2 -g -nostdlib -ffreestanding LDFLAGS = -T mps2.ld -nostdlib -nostartfiles -Wl,--gc-sections OBJS = startup.o main.o all: main.elf main.bin main.elf: $(OBJS) mps2.ld $(LD) $(LDFLAGS) -o $@ $(OBJS) %.o: %.s $(CC) $(CFLAGS) -c -o $@ $< %.o: %.c $(CC) $(CFLAGS) -c -o $@ $< main.bin: main.elf $(OBJCOPY) -O binary $@ $< clean: rm -f *.o *.elf *.bin run: main.elf qemu-system-arm -machine mps2-an505 -nographic -monitor none -kernel main.elf

跑一下:

make clean && make run

终端里应该会看到:

Hello from Cortex-M33 on QEMU MPS2-AN505

到这里,最小仿真环境已经通了。没有真实开发板,没有仿真器,就是一个纯软件的M33运行环境。QEMU窗口会一直挂在那里,因为程序是个死循环,按Ctrl+A然后按X可以退出QEMU。

4. MPU实战:加入只读区域并触发MemManage Fault

4.1 用PRIVDEFENA简化MPU初始化

现在开始加MPU。我一贯的原则是,验证一个新机制时,配置越简单越好,先把链路跑通,再谈复杂的多区域划分。

ARMv8-M的MPU_CTRL寄存器有几个关键位:

bit名称作用
bit0ENABLE全局使能MPU
bit1HFNMIENA在NMI和HardFault期间是否强制使能MPU
bit2PRIVDEFENA使能背景区域,允许特权模式访问未命中任何region的地址

PRIVDEFENA这个位特别适合学习阶段使用。如果把它置1,没有命中region的地址默认允许特权模式访问,这样我们只需要配置一个“只读”区域,就能在代码区制造一个写保护陷阱,而栈、外设、系统控制空间都还能正常工作,不会因为漏配某个外设导致莫名其妙地蹦Fault。

我规划的测试逻辑很简单:

  1. 配置一个Region,覆盖0x00000000开始的64KB代码区。
  2. 区域属性设置为Normal内存、特权模式只读。
  3. 开启MPU,同时开启PRIVDEFENA。
  4. 在main里故意向0x00000000写一个值,触发MemManage。
  5. 在MemManage_Handler里打印一行错误信息,然后死循环。

这样既能证明MPU真的在起作用,又能把故障类型直观地打出来。

4.2 故意写只读内存,验证MPU生效

Cortex-M33的MPU寄存器在系统控制空间,地址如下:

寄存器地址
MPU_CTRL0xE000ED94
MPU_RNR0xE000ED98
MPU_RBAR0xE000ED9C
MPU_RLAR0xE000EDA0
MAIR00xE000EDC0
MAIR10xE000EDC4

这里我给出一份可以直接用的MPU初始化代码,同时为了不让寄存器位操作太隐晦,我用宏定义了CMSIS风格的属性构造方式:

#define MPU_BASE 0xE000ED90UL #define MPU_CTRL (*(volatile uint32_t *)(MPU_BASE + 0x04)) #define MPU_RNR (*(volatile uint32_t *)(MPU_BASE + 0x08)) #define MPU_RBAR (*(volatile uint32_t *)(MPU_BASE + 0x0C)) #define MPU_RLAR (*(volatile uint32_t *)(MPU_BASE + 0x10)) #define MAIR0 (*(volatile uint32_t *)(0xE000EDC0UL)) #define MAIR1 (*(volatile uint32_t *)(0xE000EDC4UL)) #define ARM_MPU_AP_RW 3 #define ARM_MPU_AP_RO 5 #define ARM_MPU_ATTR_DEVICE 0x00 #define ARM_MPU_ATTR_NORMAL_NC 0x04 #define ARM_MPU_ATTR(ap, sh, memattr) \ (((ap) & 0x7U) | (((sh) & 0x1U) << 3) | (((memattr) & 0xFU) << 4)) static void mpu_init(void) { /* 先关闭MPU,避免配置过程中产生不一致状态 */ MPU_CTRL = 0; /* Attr0: Normal内存,非缓存,特权模式只读 */ MAIR0 = ARM_MPU_ATTR(ARM_MPU_AP_RO, 0, ARM_MPU_ATTR_NORMAL_NC); MAIR1 = 0; /* Region0: 0x00000000 ~ 0x0000FFE0,64KB,使用Attr0 */ MPU_RNR = 0; MPU_RBAR = (0x00000000UL & ~0x1FUL) | (0UL << 0) | (1UL << 4); MPU_RLAR = (0x0000FFE0UL & ~0x1FUL) | (0UL << 1) | (1UL << 0); /* 开启MPU,同时使能PRIVDEFENA */ MPU_CTRL = (1UL << 0) | (1UL << 2); }

这里有两个位操作要重点解释。RBAR的bit[4]是VALID,写1表示“这次写入对齐到RNR选择的region并立即使能”,所以(1UL << 4)是必须的。RLAR的bit[3:1]是AttrIndx,因为MAIR0里Attr0放在第0个字节,所以这里属性索引写0,也就是(0UL << 1)。RLAR的bit[0]是ENABLE,置1后region才真正生效。

然后是64KB区域为什么Limit要写0x0000FFE0而不是0x0000FFFF。因为ARMv8-M要求Limit地址32字节对齐,0xFFFF不满足对齐条件,硬件会直接忽略这个配置或者行为未定义。0xFFE0是64KB范围内最后一个32字节对齐的地址,从0x00000000到0x0000FFE0,这个区域就是0~64KB,没错。

4.3 用GDB确认故障原因和现场

接下来,把main函数改一下,在里面调用mpu_init,然后故意写只读区域:

void MemManage_Handler(void) { uart_puts("\r\n[MemManage] MPU fault triggered!\r\n"); while (1) { } } int main(void) { uart_init(); uart_puts("Hello from Cortex-M33 on QEMU MPS2-AN505\r\n"); mpu_init(); uart_puts("MPU enabled. Try to write read-only region...\r\n"); /* 故意向代码区0x00000000写入数据,预期触发MemManage */ *(volatile uint32_t *)0x00000000UL = 0xDEADBEEFUL; uart_puts("This line should never be printed\r\n"); while (1) { } }

这里因为MemManage_Handler在startup.s里是弱符号,C文件里定义了一个同名的强符号,链接器会优先用C文件的实现,所以不用改动startup.s。重新编译运行:

make clean && make run

输出应该是:

Hello from Cortex-M33 on QEMU MPS2-AN505 MPU enabled. Try to write read-only region... [MemManage] MPU fault triggered!

如果看到这条错误信息,说明MPU的只读保护已经生效了。程序在向0x00000000写入的时候被MPU拦下来,触发了MemManage异常,然后进入我们自己写的异常处理函数。

为了确认这不是碰巧,我们再用GDB把现场扒开看看。先启动QEMU的调试模式:

make debug

我通常会在Makefile里加一个debug目标:

debug: main.elf qemu-system-arm -machine mps2-an505 -nographic -monitor none \ -S -gdb tcp::1234 -kernel main.elf

然后用gdb-multiarch连上去:

gdb-multiarch -q build/main.elf (gdb) target remote :1234 (gdb) b MemManage_Handler (gdb) continue

程序会在进入MemManage_Handler时停下来。这时候可以看关键寄存器:

(gdb) info registers pc lr msp (gdb) x/wx 0xE000ED28

0xE000ED28是CFSR寄存器,也就是可配置故障状态寄存器。Cortex-M33的MemManage fault状态在CFSR的最低字节MMFSR里:

  • bit0 IACCVIOL:指令访问违规。
  • bit1 DACCVIOL:数据访问违规。

如果x/wx 0xE000ED28读出来bit1是1,就说明这个MemManage异常是数据访问违规引起的,正好对应我们向只读区域写0xDEADBEEF的操作:

(gdb) x/wx 0xE000ED28 0xe000ed28: 0x00000002

0x00000002就是bit1置位,DACCVIOL,100%确认是数据访问违规。这个排查思路比你单纯看“程序卡了”要靠谱得多,以后在任何Cortex-M33板子上遇到MPU问题都能用。

5. 常见坑:串口没输出、MPU一开就挂、GDB连不上

5.1 高频问题速查

我把实际折腾过程中遇到过的、以及周围朋友问过最多的问题整理成一个速查表:

现象最可能的原因解决办法
QEMU启动后终端无任何输出串口地址写错或没加-nographic确认UART0基地址是0x40004000,启动命令加-nographic
程序一使能MPU就进HardFault没有使能PRIVDEFENA,或外设/栈地址没有匹配任何region配置对应region,或临时把PRIVDEFENA置1
写了RLAR但region就是不生效Limit地址没有32字节对齐64KB区域的Limit写0x0000FFE0,不要写0x0000FFFF
向量表第一项SP不对,程序跑飞链接脚本的_estack符号没定义对确认_estack = ORIGIN(RAM) + LENGTH(RAM)
GDB连不上QEMU没加-S或端口不对启动命令加-S -gdb tcp::1234,再target remote
编译链接报undefined reference to _sdata启动文件和链接脚本符号不一致检查startup.s里引用的符号和ld脚本里定义的是否同名
修改C代码后运行结果没变make clean后重新编译裸机工程容易漏掉依赖关系,先clean一次看看

其中MPU一开就挂这个问题,新手遇到得最多。其实原因往往很简单:使能MPU之前,你所有内存访问都不受限制,一旦打开MPU,如果PRIVDEFENA是0,那么任何没被region覆盖的地址都会被拒绝访问。这时不光外设访问会触发异常,连函数调用要用的栈都可能踩在未覆盖区域里,自然一开就挂。所以我强烈建议初学时先把PRIVDEFENA打开,把背景区域放行,只在你关心的地址段上做限制,等逻辑清楚了再收紧权限。

5.2 调试手段:QEMU日志和GDB远程调试

除了GDB,QEMU自己还提供了很多调试日志选项,强烈建议掌握。比如:

qemu-system-arm -machine mps2-an505 -nographic -monitor none \ -kernel main.elf -d guest_errors

-d guest_errors会把guest访问未实现外设、非法内存访问的细节打印出来。如果程序里访问了QEMU没模拟的寄存器,或者访问了未映射地址,终端上会直接告诉你“Guest wrote to invalid address”之类信息,省去很多盲猜的时间。

还有一个更狠的调试参数:

-d in_asm

这个会把每条执行的指令反汇编打印出来,适合看程序到底“死”在哪个指令上。不过输出量非常大,一般配合-D log.txt把日志写到文件里再慢慢看。

GDB远程调试也是必会的技能。除了上面说的在MemManage_Handler打断点,还可以这样操作:

(gdb) info registers (gdb) x/8wx 0x00000000 (gdb) x/8i $pc

配合QEMU的monitor info mtree可以看整个系统的内存树:

(qemu) info mtree

这样能确认UART0、MPU寄存器、SRAM这些地址在QEMU里是否真的存在。这个方法在排查“为什么访问某个地址会挂”的时候特别好用。

5.3 还能往哪折腾:TrustZone、RTOS、ARM64用户态

这套环境跑通之后,你可以做的事情还有很多。如果你用的是mps2-an521,还可以体验Cortex-M33的TrustZone特性,在QEMU里切到非安全世界,测试SAU和MPU的联动。不要觉得TrustZone离你很远,现在不少物联网安全方案都是基于M33做的,而QEMU是少数能让你在不需要开发板的情况下完整验证这套机制的免费工具。

如果你在移植RTOS,比如FreeRTOS,M33的port里通常就有MPU支持。你可以先把内核裸跑起来,再打开MPU保护,观察任务切换时region重配置是否正常。这类问题在真机上调试非常痛苦,但在QEMU里你随时可以打断点,甚至能把MPU配置打出来逐字段核对。

再往外扩展,QEMU也可以用来模拟ARM64的用户态程序,比如在x86的Ubuntu上直接跑交叉编译的aarch64 Linux可执行文件;如果你想模拟带网络的完整ARM虚拟机,也可以把虚拟机网卡接到宿主机的tap0网桥上做二层网络调试。说到底,QEMU不止是“一个单片机模拟器”,它是一套完整的硬件虚拟化工具链,思维打通之后,M33、ARM64、RISC-V的路子都是相通的。

我个人实际测下来的体会是:QEMU模拟M33的MPU行为跟真机几乎一致,这是它最大的价值所在,特别适合理解“权限保护”这类硬件机制;但它终归不是万能的,外设模型简化比较多,做应用开发还是得留一块真实板子在手边。最后再分享一个小技巧:每次改完MPU配置,先别急着上真机,先跑一遍QEMU,再用-d guest_errors看一眼有没有异常访问,能帮你挡掉至少一半的低级错误。

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

macOS Gatekeeper深度解析:签名、公证与运行时加固

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:15:23

3个实战案例揭秘宁夏建设教育协会网站搭建避坑指南

3个实战案例揭秘宁夏建设教育协会网站搭建避坑指南 别再看那些千篇一律的模板网站了,真的不够用。上次去给一个行业协会做站,客户指着那个烂大街的Bootstrap模板说:“这看着像2015年的东西,能代表咱们协会的专业度吗?”我当场就急了,模板确实丑,但更丑的是背后的逻辑僵化,完全没法满足协会特有的业务…

作者头像 李华
网站建设 2026/9/27 1:15:14

Flash网站制作避坑指南:3个注意事项教你省50%预算

Flash网站制作避坑指南:3个注意事项教你省50%预算 找建站公司报价时,是不是总觉得对方在忽悠你?明明需求很简单,报价单却写得像天书,几千块起步,动不动就上万。别慌,这行水确实深,但只要抓住 注意事项…

作者头像 李华
网站建设 2026/9/27 1:15:06

工业扫码模组接口选型实战指南:USB-HID、虚拟串口与RS485深度对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:14:51

个人可以建设网站吗不备案怎么选

个人建站不备案避坑指南:3大注意事项保安全 别再被那些花里胡哨的模板网站忽悠了,看着光鲜,实际部署起来全是坑,尤其是“不备案”这事儿,90%的新手都栽在细节上。很多老板觉得个人搞个网站,不备案也能先上线跑跑流量,结果要么服务器被墙,要么数据全丢,最后还得花钱请人救火。…

作者头像 李华
网站建设 2026/9/27 1:14:39

底层能力:任何时代都不过时的个人护城河,从判断力到学习力

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华