嵌入式系统基础:MCU与SoC架构、存储器映射、外设与寄存器、启动流程

做固件逆向,说白了就是跟芯片底层打交道。你得先搞清楚这芯片长什么样、怎么跑起来的,才能去翻它的老底。这一节,我带你捋一捋嵌入式系统最核心的几个概念。

MCU与SoC:到底有啥区别?

很多人刚入行时容易混淆这两个词。我简单说说我的理解。

MCU(微控制器),就是一块芯片上集成了CPU、内存、各种外设。典型代表就是STM32、AVR这些。特点是:资源有限、功耗低、价格便宜。你家里的电饭煲、遥控器里基本都是它。

SoC(片上系统),比MCU复杂得多。除了CPU和内存,还可能集成GPU、DSP、NPU、Wi-Fi/蓝牙基带等等。比如高通的骁龙、博通的BCM系列。我拆过不少IoT设备,里面那颗带Linux系统的芯片,十有八九是SoC。

做逆向时,这两者的区别直接影响你的分析策略:

  • MCU固件:通常是裸机或RTOS,固件体积小(几KB到几MB),直接烧在Flash里。分析时重点关注中断向量表和启动代码。
  • SoC固件:往往跑Linux或Android,固件包含Bootloader、内核、文件系统。分析时得先拆包,找到各个分区的入口。

我个人习惯:拿到一个陌生设备,先看芯片型号。如果是STM32F103这种,直接按MCU思路来;如果是全志V3s这种,就得准备分析U-Boot和内核了。

存储器映射:芯片的“地图”

每个嵌入式芯片都有一个存储器映射。说白了,就是芯片厂商把Flash、SRAM、各个外设的寄存器,统统映射到一个统一的地址空间里。你想想看,CPU访问Flash、读写寄存器,本质上都是对某个地址进行读写操作。

以经典的STM32F103为例,它的存储器映射大致长这样:

起始地址 结束地址 区域 说明
0x0000 0000 0x0000 FFFF 别名区/重映射区 启动时映射到Flash或系统存储器
0x0800 0000 0x0801 FFFF 主Flash 存放用户代码和常量
0x1FFF F000 0x1FFF F7FF 系统存储器 存放Bootloader(出厂固件)
0x2000 0000 0x2000 4FFF SRAM 运行时数据、堆栈
0x4000 0000 0x4002 3FFF 外设寄存器 GPIO、USART、TIM等控制寄存器

避坑指南:我曾经逆向一个智能插座,死活找不到固件入口。后来查了芯片手册才发现,它的Flash基地址不是0x08000000,而是0x00000000。嗯,这里要注意,不同厂商、不同系列的芯片,映射方式可能完全不同。拿到固件第一件事,就是去查对应芯片的Reference Manual

外设与寄存器:控制硬件的“开关”

外设就是芯片上除了CPU之外的功能模块,比如GPIO、UART、I2C、SPI、定时器、ADC等等。每个外设都有一组寄存器,CPU通过读写这些寄存器来控制外设的行为。

举个例子,你想让一个GPIO引脚输出高电平,流程大概是:

  1. 找到该GPIO对应的基地址(比如GPIOA的基地址是0x40010800)。
  2. 配置模式寄存器(如CRL/CRH),把引脚设为推挽输出。
  3. 写输出数据寄存器(ODR),把对应位置1。

在逆向固件时,识别外设寄存器的访问模式非常关键。我常用的方法是:

  • 在固件中搜索外设基地址附近的常量。比如看到0x40010800,基本可以断定是在操作GPIOA。
  • 分析代码中对这些地址的读写序列。比如先写配置寄存器,再写数据寄存器,这就是典型的GPIO初始化流程。

注意:有些芯片为了安全,会对外设寄存器做写保护。比如写某个寄存器前,必须先往一个“解锁”寄存器写入特定密钥。我在分析一个工业控制器固件时,就遇到过这种设计。当时卡了我两天,后来发现是没解锁导致外设配置不生效。

启动流程:芯片的“第一口奶”

芯片上电后,CPU从哪开始执行代码?这就是启动流程要回答的问题。不同架构的启动流程略有差异,但核心逻辑是相通的。

以ARM Cortex-M系列为例,启动流程是这样的:

  1. 上电复位:CPU从0x00000000(或映射后的地址)读取栈顶指针(MSP),存入SP寄存器。
  2. 读取复位向量:从0x00000004读取复位中断向量,存入PC寄存器。
  3. 跳转执行:CPU跳转到复位向量指向的地址,开始执行启动代码(startup_xxx.s)。
  4. 系统初始化:启动代码会做一堆杂事——设置时钟、初始化堆栈、拷贝数据段、清零BSS段。
  5. 调用main():最后跳转到C语言的main函数,进入用户程序。

我画了一张图,帮你把整个流程串起来:

ARM Cortex-M 启动流程 1. 上电复位 读取 0x00000000 → MSP 2. 读取复位向量 读取 0x00000004 → PC 3. 跳转执行 执行启动代码 4. 系统初始化 设置时钟 → 初始化堆栈 → 拷贝数据段 → 清零BSS 5. 调用 main() 进入用户程序 关键数据结构 中断向量表 栈顶指针 (MSP) 复位向量 (Reset_Handler) 其他异常向量

逆向固件时,启动代码是第一个要分析的目标。我通常会在IDA Pro里直接定位到复位向量指向的地址,然后逐条分析汇编指令。你会发现,很多固件的启动代码都是芯片厂商提供的模板,改动不大。但有些设备厂商会在这里加一些“私货”——比如校验固件完整性、解密下一级代码。

我的经验:有一次我逆向一个路由器固件,发现启动代码里有一段诡异的循环,反复读取某个GPIO引脚的电平。后来查电路图才知道,那是检测某个硬件跳线是否短接,用来决定进入“救援模式”还是正常启动。这种设计在工业设备里很常见,逆向时多留个心眼。

小结

这一节的内容,说白了就是让你建立对嵌入式系统的基本认知。MCU和SoC的区别决定了分析工具和思路;存储器映射是你在固件里“导航”的地图;外设寄存器是控制硬件的开关;启动流程则是固件执行的起点。把这几个点吃透了,后面分析固件结构、定位漏洞时,你才能游刃有余。

嗯,今天就先聊到这儿。下一节我们开始动手,看看怎么把固件从芯片里“请”出来。


公众号:蓝海资料掘金营,微信deep3321