第二章 嵌入式系统硬件基础:你得先懂芯片怎么干活

做固件逆向和性能调优,说白了就是跟硬件打交道。你连芯片怎么启动、外设怎么访问都不清楚,那分析起二进制代码来,就像蒙着眼走路。这一章,我把最核心的硬件基础给你捋一遍。

2.1 常见MCU/CPU架构:三足鼎立

目前嵌入式领域,ARM、MIPS、RISC-V 这三家占了绝大多数。我个人的习惯是,拿到一个固件,先看它是哪种架构——这决定了指令集、寄存器模型和后续的分析工具。

ARM:生态最成熟

ARM 是当前嵌入式市场的绝对主力。从 Cortex-M 系列(单片机)到 Cortex-A 系列(应用处理器),覆盖极广。我记得刚入行时,第一个逆向的项目就是基于 ARM Cortex-M3 的蓝牙耳机固件。ARM 的特点是:指令定长(Thumb/Thumb-2)与变长(ARM)混合,寄存器丰富,有专门的 LR(链接寄存器)和 PC(程序计数器)。

避坑指南: 我曾经在分析一个 ARM 固件时,一直按 ARM 模式反汇编,结果全是乱码。后来才发现,芯片上电默认从 Thumb 模式启动。记住:Cortex-M 系列只支持 Thumb/Thumb-2 指令集,没有 ARM 模式。

MIPS:简洁但坑多

MIPS 在路由器、网关设备里非常常见。它的指令格式非常规整,每条指令都是 32 位。但 MIPS 有个著名的 延迟槽(Delay Slot) 问题——分支指令后面的那条指令无论分支是否跳转,都会被执行。这给逆向分析带来了不少麻烦。

你想想看,反汇编时看到跳转指令后面紧跟着一条看似无关的指令,很容易分析错。我调试过一个 MIPS 的交换机固件,就因为没注意延迟槽,导致函数调用关系图画错了三天。

RISC-V:新贵崛起

RISC-V 是开源指令集架构,这几年发展极快。它的设计非常精简,模块化程度高。做 RISC-V 的逆向,好处是指令集手册清晰,坏处是生态还在建设中,工具链支持不如 ARM 成熟。我最近在做一个 RISC-V 的 IoT 安全网关项目,感觉它的可扩展性确实强,但分析工具还得自己写一些脚本。

特性 ARM (Cortex-M) MIPS (32位) RISC-V (RV32)
指令长度 16/32位混合 固定32位 16/32位混合
通用寄存器 16个 (R0-R15) 32个 32个
延迟槽 无 有 无
中断响应 硬件自动压栈 软件处理 可配置
逆向难度 中等(工具多) 较高(延迟槽) 中等(工具少)

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

每个 MCU 上电后,CPU 看到的是一张固定的地址地图——这就是存储器映射。Flash、RAM、外设寄存器,都挂在这张地图上。做逆向时,第一件事就是找到这个芯片的 Memory Map 表,通常在数据手册的第 2 章或第 3 章。

举个例子,STM32F103 的存储器映射:

  • 0x0000 0000 - 0x0000 FFFF:别名区或 Boot 区
  • 0x0800 0000 - 0x0801 FFFF:主 Flash(128KB)
  • 0x2000 0000 - 0x2000 4FFF:SRAM(20KB)
  • 0x4000 0000 - 0x4002 3FFF:外设寄存器区

寄存器说白了就是外设的控制手柄。你往某个地址写一个值,外设就干活了。比如,设置 GPIO 的输出电平,就是往 GPIOx_ODR 寄存器写数据。我习惯在逆向时,先把关键外设的寄存器基地址和偏移量整理成表格,这样看汇编代码时,一眼就能认出这是在操作哪个外设。

关键点: 固件中访问外设的代码,通常表现为 LDR(加载地址) + STR(存储数据) 的组合。如果你在反汇编中看到 0x4000xxxx 这样的地址,基本可以断定是在操作外设寄存器。

2.3 外设与总线:芯片怎么跟外界说话

嵌入式芯片不是孤岛,它得通过总线连接各种外设。UART、I2C、SPI 是三大最常用的串行总线。做性能调优时,总线速率和协议开销往往是瓶颈。

UART:最朴实的串口

UART 是异步串行通信,只需要 TX、RX 两根线。我调试固件时,第一件事就是找 UART 的调试串口——这是逆向的“生命线”。通过监听 UART 输出,你能拿到系统日志、shell 甚至敏感信息。

UART 的波特率设置很关键。常见的波特率有 9600、115200、921600。我曾经遇到一个设备,它的 UART 输出波特率是 1843200,标准工具扫不出来,折腾了半天才发现是自定义波特率。

I2C:多设备总线

I2C 只有 SDA(数据)和 SCL(时钟)两根线,但可以挂载多个设备,每个设备有唯一的 7 位或 10 位地址。I2C 的速率通常为 100kHz(标准模式)或 400kHz(快速模式)。

逆向 I2C 通信时,我一般用逻辑分析仪抓波形,然后解析出设备地址和寄存器地址。比如,一个温度传感器,主机先发设备地址 + 写位,再发寄存器地址,然后重新发起读操作,从机返回温度数据。

SPI:高速通道

SPI 是全双工同步通信,需要四根线:SCK、MOSI、MISO、CS。SPI 的速度可以跑到几十 MHz,适合传输大量数据,比如 Flash、LCD 屏、SD 卡。

SPI 的片选信号(CS)是重点。每个从设备独占一根 CS 线。在逆向分析时,如果你看到代码中频繁操作某个 GPIO 拉低再拉高,那很可能就是在控制 SPI 的片选。

总线 线数 通信方式 典型速率 典型设备
UART 2 (TX/RX) 异步 115200 bps 调试串口、GPS 模块
I2C 2 (SDA/SCL) 同步半双工 100-400 kHz 传感器、EEPROM
SPI 4 (SCK/MOSI/MISO/CS) 同步全双工 1-50 MHz Flash、LCD、SD 卡

2.4 启动流程与Bootloader:芯片的第一口奶

芯片上电后,CPU 怎么知道自己该干什么?这就涉及到启动流程。不同架构的启动流程略有差异,但核心思想一致:从复位向量取地址,然后跳转执行。

ARM Cortex-M 启动流程

Cortex-M 的启动流程非常优雅。上电后,CPU 自动从 0x0000 0000 加载栈指针(MSP),从 0x0000 0004 加载复位向量(PC 的初始值)。然后跳转到复位向量指向的地址,执行启动代码。

启动代码通常用汇编编写,主要做三件事:

  1. 初始化栈指针
  2. 拷贝 .data 段到 RAM,清零 .bss 段
  3. 调用 main() 函数

逆向时,找到复位向量表非常关键。我一般会在固件二进制文件中搜索 0x2000xxxx 这样的栈顶地址,然后顺藤摸瓜找到整个向量表。

注意: 有些芯片支持从不同的存储介质启动(如 Flash、SRAM、System Memory)。启动模式由 BOOT0/BOOT1 引脚的电平决定。如果逆向时发现固件行为异常,先检查启动模式配置。

Bootloader:固件的“接生婆”

Bootloader 是芯片上电后第一个执行的用户程序。它的职责是:初始化硬件、检测启动条件、加载并跳转到主固件。常见的 Bootloader 有 U-Boot、MCUboot 等。

Bootloader 的逆向分析价值极高。因为它包含了硬件初始化的完整流程——时钟配置、DDR 初始化、外设使能等。我分析一个陌生设备时,总是先反汇编 Bootloader,把硬件初始化流程摸清楚,再分析主固件就轻松多了。

举个例子,U-Boot 的启动流程大致如下:

  • Stage 1 (SPL):初始化 CPU、时钟、DDR,加载 Stage 2
  • Stage 2 (U-Boot proper):初始化更多外设,加载内核或固件

在逆向 Bootloader 时,我习惯关注以下几个关键点:

  • 时钟配置寄存器(PLL、分频器)
  • DDR 控制器初始化序列
  • Flash 控制器配置
  • 跳转到主固件的地址
嵌入式系统启动流程总览 芯片上电复位 硬件加载复位向量 (0x0000 0004) Bootloader 执行 初始化时钟/DDR/外设 从 Flash 加载主固件 跳转到主固件入口 逆向关注点 复位向量表位置 逆向关注点 Bootloader 初始化序列 逆向关注点 固件加载地址与校验
实战技巧: 我分析 Bootloader 时,会重点关注 SCB->VTOR 寄存器(向量表偏移寄存器)的写入操作。这个寄存器决定了中断向量表的位置。如果 Bootloader 修改了 VTOR,说明它可能重映射了中断向量表,主固件的中断处理函数地址也会随之变化。