嵌入式系统架构基础:ARM Cortex-M/R/A、MIPS与RISC-V
搞固件逆向,说白了就是跟芯片架构打交道。你连CPU怎么取指令、怎么访问外设都不清楚,那逆向出来的汇编代码就是天书。这一章我带你过一遍主流嵌入式架构的核心差异,这些都是我这些年踩坑踩出来的经验。
ARM Cortex-M:微控制器的王者
Cortex-M系列是我最常遇到的。从智能家电到工业控制器,遍地都是。它的特点是单周期指令、向量表中断、Thumb/Thumb-2指令集。
我个人习惯,拿到固件先看0x0000_0000地址。为什么?因为M核上电后从那里取栈指针,然后从0x0000_0004取复位向量。这个向量表结构是固定的,一眼就能认出来。
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核。
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的漏洞利用代码时,一直没搞懂为什么跳转后还执行了那条指令,后来才想起延迟槽这回事。
- $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。
下面这张图展示了嵌入式系统固件逆向中,架构知识的核心脉络:
最后说一句,内存映射不是死的。很多芯片支持重映射——比如上电时从Flash启动,但运行后可以把向量表搬到SRAM。逆向时如果发现代码在修改SCB->VTOR或者类似寄存器,就要警惕了,内存布局已经变了。