嵌入式系统基础:MCU与SoC架构、常见外设与启动流程

做固件逆向,第一步得搞清楚你面对的是什么硬件。我见过不少新手,拿到一个二进制文件就急着反汇编,结果连芯片型号都没看——这就像修车不看发动机型号,直接拆螺丝,多半要翻车。

嵌入式系统里,最核心的两个概念就是MCU和SoC。很多人觉得它们差不多,其实差别挺大。我简单说说我的理解。

MCU vs SoC:到底有啥区别?

MCU(微控制器),说白了就是一块芯片上集成了CPU、内存、外设接口。它是个完整的微型计算机系统。你想想看,一个智能手环里的主控芯片,通常就是MCU。它资源有限,但功耗极低,实时性好。

SoC(片上系统),则更复杂。它除了CPU核心,还可能集成GPU、DSP、NPU、视频编解码器等等。手机里的骁龙芯片就是典型的SoC。SoC的启动流程比MCU复杂得多,往往有多个启动阶段。

我个人习惯这样区分:MCU跑裸机或RTOS,SoC跑Linux或Android。当然这不是绝对的,但八九不离十。

核心要点:逆向MCU固件,你主要关注的是寄存器、外设地址、中断向量表。逆向SoC固件,你还要关注BootROM、引导加载程序(如U-Boot)、设备树等。

常见外设:UART / I2C / SPI

这三个接口,是嵌入式系统里最常用的通信方式。做逆向时,识别它们能帮你快速定位关键代码。

UART(通用异步收发器)

UART是最简单的串行通信接口。两根线:TX(发送)、RX(接收)。异步,意味着没有时钟线,双方得约定好波特率。

我在项目中遇到过,很多IoT设备出厂时,UART口是默认开启的。如果你能找到板子上的UART测试点,用逻辑分析仪抓一下,往往能直接拿到调试信息,甚至进入shell。嗯,这里要注意:有些厂商会故意把UART引脚复用成GPIO,或者干脆在BootROM里禁用掉。

逆向小技巧:在固件二进制中搜索连续的0x55或0xAA字节序列,这通常是UART的空闲或同步字节。另外,中断向量表中UART中断处理函数的地址,也能帮你定位UART驱动代码。

I2C(Inter-Integrated Circuit)

I2C用两根线:SCL(时钟)、SDA(数据)。它支持多主多从,每个设备有唯一的7位或10位地址。速度不快(标准模式100kHz,快速模式400kHz),但胜在简单,常用于连接传感器、EEPROM、RTC等。

我记得有一次逆向一个智能门锁,发现它用I2C连接了一个触摸芯片。通过分析I2C读写函数,我找到了触摸数据的处理逻辑,进而还原了密码验证的流程。说白了,I2C的驱动代码通常就是几个固定的模式:起始信号、发送地址、读写数据、停止信号。

SPI(串行外设接口)

SPI比I2C快得多,全双工,四根线:SCK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。它常用于Flash、SD卡、LCD显示屏等高速设备。

做固件逆向时,SPI Flash尤其重要。因为很多MCU的固件就存在外部SPI Flash里。你如果能从板子上把Flash吹下来,用编程器读出内容,那整个固件就到手了。我曾经用这个方法,从一个路由器里提取出了完整的Linux内核和文件系统。

接口 线数 速度 典型用途 逆向关注点
UART 2 慢(115200bps常见) 调试、日志、控制台 波特率、中断处理、字符串输出
I2C 2 中(100-400kHz) 传感器、EEPROM、RTC 设备地址、读写时序、寄存器配置
SPI 4+ 快(数十MHz) Flash、SD卡、LCD 片选信号、DMA传输、Flash命令集

存储器映射与启动流程

这是嵌入式逆向的基石。不理解存储器映射,你连代码在哪儿跑的都搞不清楚。

存储器映射

每个MCU或SoC,都会把Flash、RAM、外设寄存器映射到统一的地址空间里。比如STM32F103,Flash从0x08000000开始,RAM从0x20000000开始,外设从0x40000000开始。

你拿到一个固件,第一件事就是查芯片手册,找到它的存储器映射图。然后根据固件大小和加载地址,判断它是从Flash直接运行,还是需要拷贝到RAM里运行。

注意:有些芯片支持“零等待”Flash访问,有些则不行。如果代码在Flash里运行太慢,启动代码会把它搬到RAM里。这种“搬运”逻辑,往往是逆向分析的关键入口点。

启动流程

MCU的启动流程相对简单。以ARM Cortex-M为例:

  1. 上电复位后,CPU从地址0x00000000读取栈指针(MSP)初始值。
  2. 从地址0x00000004读取复位向量,即第一条指令的地址。
  3. 跳转到复位向量执行,通常是启动代码(startup_xxx.s)。
  4. 启动代码初始化全局变量、设置时钟、初始化外设,最后跳转到main()。

SoC的启动流程就复杂多了。以全志V3s为例:

  1. 芯片上电,BootROM(固化在芯片内部)开始执行。
  2. BootROM从SD卡、NAND Flash或SPI Flash中读取SPL(Secondary Program Loader)。
  3. SPL初始化DRAM,然后从存储介质中加载U-Boot。
  4. U-Boot加载设备树(DTB)和Linux内核。
  5. Linux内核启动,挂载根文件系统。

你想想看,逆向SoC固件时,如果你能找到BootROM的代码(有些芯片会开放),那就能理解整个启动链的信任根。但大多数时候,BootROM是保密的,你只能从SPL或U-Boot入手。

我的经验:分析启动流程时,重点关注“从哪里加载”、“加载到哪个地址”、“跳转到哪里”。这三个问题搞清楚了,整个固件的脉络就清晰了。我曾经逆向过一个智能摄像头,发现它的U-Boot里有一段校验逻辑,专门检查内核镜像的签名。绕过这个校验,就能刷入自定义固件。

MCU典型启动流程(ARM Cortex-M) 上电复位 读取0x00000000处的MSP 读取复位向量 从0x00000004获取PC初值 跳转到启动代码 startup_xxx.s 初始化硬件 时钟、外设、全局变量 跳转到main() 用户应用程序 时间轴 → 注:实际启动流程可能因芯片厂商而异,但核心逻辑一致

好了,这一章的内容就到这里。嵌入式系统的基础,说白了就是搞清楚芯片怎么启动、外设怎么通信、内存怎么布局。这些概念搞明白了,后面分析固件才能得心应手。


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