第二章 嵌入式系统硬件基础:你得先懂芯片怎么干活
做固件逆向和性能调优,说白了就是跟硬件打交道。你连芯片怎么启动、外设怎么访问都不清楚,那分析起二进制代码来,就像蒙着眼走路。这一章,我把最核心的硬件基础给你捋一遍。
2.1 常见MCU/CPU架构:三足鼎立
目前嵌入式领域,ARM、MIPS、RISC-V 这三家占了绝大多数。我个人的习惯是,拿到一个固件,先看它是哪种架构——这决定了指令集、寄存器模型和后续的分析工具。
ARM:生态最成熟
ARM 是当前嵌入式市场的绝对主力。从 Cortex-M 系列(单片机)到 Cortex-A 系列(应用处理器),覆盖极广。我记得刚入行时,第一个逆向的项目就是基于 ARM Cortex-M3 的蓝牙耳机固件。ARM 的特点是:指令定长(Thumb/Thumb-2)与变长(ARM)混合,寄存器丰富,有专门的 LR(链接寄存器)和 PC(程序计数器)。
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 的初始值)。然后跳转到复位向量指向的地址,执行启动代码。
启动代码通常用汇编编写,主要做三件事:
- 初始化栈指针
- 拷贝 .data 段到 RAM,清零 .bss 段
- 调用
main()函数
逆向时,找到复位向量表非常关键。我一般会在固件二进制文件中搜索 0x2000xxxx 这样的栈顶地址,然后顺藤摸瓜找到整个向量表。
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 控制器配置
- 跳转到主固件的地址
SCB->VTOR 寄存器(向量表偏移寄存器)的写入操作。这个寄存器决定了中断向量表的位置。如果 Bootloader 修改了 VTOR,说明它可能重映射了中断向量表,主固件的中断处理函数地址也会随之变化。