嵌入式系统硬件基础:MCU/CPU架构、存储器映射与启动流程
做固件逆向分析,第一关就是搞懂硬件。你连芯片怎么启动、外设怎么访问都不知道,那分析起二进制来就像盲人摸象。我个人习惯,拿到一块新板子,第一件事不是看代码,而是翻芯片手册里的存储器映射图和启动流程。这章咱们就把这些基础打牢。
常见MCU/CPU架构:三足鼎立
目前嵌入式领域,你大概率会遇到这三种架构:ARM Cortex-M、RISC-V 和 Xtensa。它们各有各的脾气。
| 架构 | 典型代表 | 指令集特点 | 我的看法 |
|---|---|---|---|
| ARM Cortex-M | STM32、nRF52、GD32 | Thumb/Thumb-2,变长指令 | 生态最成熟,资料最多,逆向工具链最全 |
| RISC-V | GD32V、CH32V、ESP32-C | 基础整数指令集RV32I,模块化扩展 | 开源,指令规整,逆向起来反而清爽 |
| Xtensa | ESP32、ESP8266 | 可配置、可变长指令 | 乐鑫家的看家本领,指令编码有点绕 |
为什么说这个重要?你逆向的时候,第一眼就要识别出架构。ARM的向量表在0x00000000,RISC-V的复位向量在0x80000000附近,这都是硬线索。我在分析一个ESP32的固件时,一开始没注意它是Xtensa架构,用ARM的反汇编工具去拆,结果全是乱码。嗯,白忙活了两小时。
存储器映射与总线结构
说白了,MCU就是一块地址空间。从0x00000000到0xFFFFFFFF,每个地址要么是Flash,要么是SRAM,要么是外设寄存器。你想想看,CPU怎么知道去哪个地址取指令?全靠这个映射表。
典型Cortex-M3存储器映射(以STM32F1为例)
- 0x00000000 - 0x000FFFFF:别名区/Flash映射,用于启动
- 0x08000000 - 0x080FFFFF:主Flash,代码存放地
- 0x20000000 - 0x2000FFFF:SRAM,栈和堆
- 0x40000000 - 0x400FFFFF:外设总线,GPIO、UART、TIM等寄存器
这里有个坑。我曾经逆向一个固件,发现代码里直接写地址0x40021000,我当时就知道这是RCC寄存器。但如果你不熟悉映射,看到这种地址会一头雾水。所以,我建议你把常见MCU的映射表背下来,至少知道Flash、SRAM、外设的大致范围。
外设寄存器与MMIO
外设寄存器,就是通过内存地址来控制的硬件。你往某个地址写一个值,GPIO就输出高电平了。这就是MMIO(Memory-Mapped I/O)。
逆向分析时,你会频繁看到类似这样的代码:
// 假设这是从固件中反编译出来的伪代码
*(volatile uint32_t*)0x40010C00 |= (1 << 12); // 设置GPIOB_ODR的第12位
看到0x40010C00,你要能反应过来:这是GPIOB的输出数据寄存器。怎么做到的?靠查手册。我习惯在IDA Pro或Ghidra里,把常见外设的基地址和寄存器偏移量做成注释,这样分析起来快很多。
我的小技巧:把芯片手册里的寄存器描述截图,或者做成表格,放在逆向工程的工作目录下。遇到MMIO访问,直接对照。省时省力。
启动流程与Bootloader
芯片上电后干了什么?这是逆向分析的起点。以Cortex-M为例,启动流程非常固定:
- 从0x00000000读取栈顶指针(MSP)
- 从0x00000004读取复位向量,跳转执行
- 执行SystemInit,配置时钟
- 跳转到main
RISC-V稍有不同。它从复位向量地址(通常是0x80000000)开始执行,没有向量表的概念,异常处理用mtvec寄存器指定。
Bootloader就更关键了。很多设备都有两级启动:一级Bootloader(固化在ROM里)负责检测启动条件,二级Bootloader(存在Flash里)负责加载固件或进入升级模式。
避坑指南:我曾经逆向一个IOT设备,发现固件里有一段代码在0x1FFF0000附近执行。我一开始以为是用户代码,后来才意识到这是芯片内部的系统Bootloader。差点把系统固件当成应用固件来分析。所以,一定要区分清楚哪些是芯片自带的,哪些是用户写的。
知识结构图:嵌入式硬件基础全景
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速索引。
小结
这一章的内容,说白了就是嵌入式硬件的“世界观”。你知道了芯片怎么划分地址空间,外设怎么控制,系统怎么启动,后面分析固件时才能有的放矢。别小看这些基础,我见过太多人上来就反汇编,结果连代码在Flash还是SRAM里都分不清,那分析出来的东西能准吗?