嵌入式系统架构基础:ARM Cortex-M/R/A、MIPS与RISC-V

搞固件逆向,说白了就是跟芯片架构打交道。你连CPU怎么取指令、怎么访问外设都不清楚,那逆向出来的汇编代码就是天书。这一章我带你过一遍主流嵌入式架构的核心差异,这些都是我这些年踩坑踩出来的经验。

核心观点: 架构决定了固件的加载方式、内存布局和指令编码。逆向的第一步,就是先判断目标是什么核。

ARM Cortex-M:微控制器的王者

Cortex-M系列是我最常遇到的。从智能家电到工业控制器,遍地都是。它的特点是单周期指令、向量表中断、Thumb/Thumb-2指令集。

我个人习惯,拿到固件先看0x0000_0000地址。为什么?因为M核上电后从那里取栈指针,然后从0x0000_0004取复位向量。这个向量表结构是固定的,一眼就能认出来。

避坑指南: 我曾经在分析一个STM32固件时,发现复位向量指向的地址不在Flash范围内。后来才发现是Bootloader把向量表重映射到了SRAM。所以别死盯着0x0000_0000,要检查VTOR寄存器。

Cortex-M的寄存器组很简单:R0-R12通用寄存器,R13是栈指针(MSP/PSP),R14是链接寄存器,R15是程序计数器。特殊寄存器有xPSR、PRIMASK等。逆向时重点关注LR的值,它经常被用来判断当前是中断上下文还是线程上下文。

特性 Cortex-M0/M0+ Cortex-M3/M4 Cortex-M7
指令集 Thumb(无Thumb-2) Thumb-2 Thumb-2 + 浮点
流水线 2级 3级 6级(双发射)
中断延迟 16周期 12周期 12周期
位带操作 无 有 有

ARM Cortex-R:实时控制的首选

Cortex-R系列你可能见得少一些,但它才是真正的硬实时王者。我做过一个汽车ECU的逆向,里面就是Cortex-R5。它跟M核最大的区别是:支持MPU(内存保护单元)、支持双核锁步、有硬件中断栈。

R核的异常模型比M核复杂。它有IRQ、FIQ、Abort、Undef等多种异常模式,每种模式都有自己的栈指针。逆向时如果看到频繁的CPSR切换和模式保存,十有八九是R核。

注意: R核的指令集是完整的ARM指令集(32位),不是Thumb。这意味着每条指令都是4字节,跟M核的2字节/4字节混合编码完全不同。反汇编时选错指令集,出来的全是乱码。

ARM Cortex-A:应用处理器的担当

Cortex-A系列跑Linux/Android,带MMU,有L1/L2 Cache。逆向A核固件,你面对的不再是裸机程序,而是Bootloader(U-Boot)、内核、设备树、文件系统。

我记得第一次逆向一个A7的启动固件,上来就懵了——怎么地址空间这么大?后来才明白,A核有MMU,物理地址和虚拟地址是两码事。你看到的0x8000_0000,可能是映射到物理0x4000_0000的。

A核的异常模型更复杂:SVC、HYP、MON、UND、ABT、IRQ、FIQ、SYS。每个模式都有独立的SP和LR。逆向时如果看到异常向量表在0xFFFF_0000或0x0000_0000,基本就是A核。

MIPS架构:曾经的RISC标杆

MIPS在路由器、机顶盒里还很常见。它的指令编码非常规整——所有指令都是32位,操作码固定在前6位。逆向MIPS固件,我最头疼的是延迟槽。

什么是延迟槽?就是分支指令后面的那条指令,无论分支是否跳转,它都会被执行。你想想看,这多坑人。我曾经在分析一个MIPS的漏洞利用代码时,一直没搞懂为什么跳转后还执行了那条指令,后来才想起延迟槽这回事。

MIPS寄存器约定:
  • $0:恒为0
  • $ra ($31):返回地址
  • $sp ($29):栈指针
  • $gp ($28):全局指针
  • $a0-$a3 ($4-$7):参数寄存器
  • $v0-$v1 ($2-$3):返回值

RISC-V架构:开源新贵

RISC-V这几年火得不行。它的设计哲学就是简洁——基础指令集只有40多条指令。逆向RISC-V固件,你会发现它跟MIPS有点像,但没有延迟槽,舒服多了。

RISC-V的寄存器叫x0-x31。x0恒为0,x1是返回地址,x2是栈指针。有意思的是,RISC-V的指令编码是可变长度的——标准32位指令,但压缩指令集(RVC)是16位。这跟ARM的Thumb有点像,但编码方式完全不同。

我建议你重点关注RISC-V的CSR(控制和状态寄存器)。逆向时看到csrrw、csrrs这些指令,就是在操作特权级寄存器。mstatus、mtvec、mepc这些是必看的。

内存映射与外设寄存器

搞逆向,内存映射图就是你的地图。没有它,你就是在黑灯瞎火里乱撞。

每个芯片的数据手册里都有内存映射图。以STM32F4为例:

起始地址 结束地址 外设/内存 大小
0x0000_0000 0x0000_FFFF Flash(别名/映射) 64KB
0x0800_0000 0x080F_FFFF Flash(主存储) 1MB
0x2000_0000 0x2001_FFFF SRAM 128KB
0x4000_0000 0x4002_3FFF APB1外设 144KB
0x4002_4000 0x4005_FFFF APB2外设 240KB
0xE000_0000 0xE00F_FFFF 系统控制块(SCB) 1MB

外设寄存器就是一堆内存地址。每个寄存器有特定的位定义。比如GPIO的ODR寄存器,写1就是输出高电平。逆向时,看到代码往0x4002_0014写值,你就知道它在操作GPIOA的ODR。

我的习惯: 逆向前先准备好目标芯片的参考手册,把关键外设的基地址记下来。然后在反汇编里搜索这些地址的引用,很快就能定位到外设初始化代码。

下面这张图展示了嵌入式系统固件逆向中,架构知识的核心脉络:

嵌入式架构知识体系 嵌入式系统架构 ARM Cortex-M ARM Cortex-R/A MIPS RISC-V 向量表 Thumb-2 MMU/MPU 异常模式 延迟槽 协处理器 CSR寄存器 核心:内存映射 + 外设寄存器 = 逆向地图 先判断架构 → 再定位内存映射 → 最后分析外设操作

最后说一句,内存映射不是死的。很多芯片支持重映射——比如上电时从Flash启动,但运行后可以把向量表搬到SRAM。逆向时如果发现代码在修改SCB->VTOR或者类似寄存器,就要警惕了,内存布局已经变了。

实战提醒: 我曾经逆向一个加密固件,发现它把整个Flash内容复制到SRAM,然后修改向量表指向SRAM,最后擦除Flash。这样你即使dump出Flash,也拿不到真正的代码。遇到这种情况,要在SRAM里找,或者hook住复制函数。