2. 嵌入式系统基础:常见嵌入式架构、启动流程、内存映射与外设
做固件逆向分析,说白了你得先懂硬件是怎么醒过来的。我见过不少搞软件安全的兄弟,一上来就盯着二进制看,结果连 Bootloader 在哪个地址跑都搞不清楚。嗯,这章咱们就把地基打牢。
2.1 常见嵌入式架构:ARM、MIPS、RISC-V
目前市面上主流的嵌入式处理器架构就这三家。我个人的习惯是,拿到一个固件先看它是哪种指令集,这决定了后面反汇编工具的选择和分析思路。
| 架构 | 指令集特点 | 字节序 | 典型应用 | 逆向难度 |
|---|---|---|---|---|
| ARM | 32/64位,Thumb/Thumb-2混合指令 | 小端为主(可切换) | 手机SoC、路由器、IoT | 中等(资料多) |
| MIPS | 32/64位,延迟槽、固定指令长度 | 大端/小端均有 | 路由器、光猫、交换机 | 较高(延迟槽坑多) |
| RISC-V | 32/64位,模块化扩展 | 小端 | 新兴IoT、AI加速器 | 中等(生态在完善) |
2.2 ARM 架构要点
ARM 是我接触最多的架构。它有两种主要执行状态:ARM 状态(4字节指令)和 Thumb 状态(2字节指令)。你想想看,一个函数可能前半段是 ARM,后半段切到 Thumb,这在逆向时特别容易跟丢。
我习惯用 objdump -d 配合 -M force-thumb 来强制反汇编。但更靠谱的方法是看函数地址的最低位:如果地址是奇数,说明是 Thumb 代码。
// 示例:ARM 与 Thumb 切换检测
// 地址 0x8000 是 ARM 代码
// 地址 0x8001 是 Thumb 代码(最低位为1)
void (*func_ptr)(void) = (void*)0x8001;
func_ptr(); // 处理器自动切换到 Thumb 状态
2.3 MIPS 架构要点
MIPS 有个让新手抓狂的特性——延迟槽。就是说分支指令后面的那条指令,不管分支跳不跳,都会先执行完。我在分析一个交换机固件时,就因为没理解延迟槽,把控制流图画错了。
// MIPS 延迟槽示例
beq $t0, $t1, target // 如果相等则跳转
addi $t2, $t2, 1 // 这条指令在跳转前一定会执行!
// target:
nop
MIPS 的寄存器命名也跟 ARM 不同:$a0-$a3 是参数,$v0-$v1 是返回值,$ra 是返回地址。嗯,这里要注意,MIPS 没有硬件堆栈,函数返回全靠 $ra。
2.4 RISC-V 架构要点
RISC-V 是后起之秀,我最近两年在 IoT 设备里见到它的频率越来越高。它的指令集是模块化的:RV32I 是基础整数指令集,RV32M 是乘除法扩展,RV32F 是单精度浮点。
逆向 RISC-V 时有个好消息:它的指令编码非常规整,不像 ARM 那样有 Thumb 切换的烦恼。但坏消息是,目前商业反汇编工具对 RISC-V 的支持还不够完善。
ra(x1)是返回地址寄存器,sp(x2)是栈指针。但有些编译器会优化掉 ra 的保存,导致调用栈回溯困难。
2.5 启动流程:从复位到操作系统
嵌入式系统的启动流程,说白了就是处理器从复位向量开始,一步步把控制权交给操作系统。我画了一张图,把整个过程串起来。
启动流程大致分这几步:
- 复位向量: 处理器上电后,从固定地址(通常是 0x00000000 或 0xFFFF0000)取第一条指令。
- Boot ROM: 芯片内部固化的只读代码,负责初始化时钟、内存控制器,然后加载 Bootloader。
- Bootloader: 比如 U-Boot、Barebox,负责初始化外设、加载操作系统。这是逆向分析的重灾区。
- 分支决策: Bootloader 检查按键或标志位,决定进入升级模式还是正常启动。
- 最终加载: 升级模式下接收新固件;正常模式下加载内核并跳转。
2.6 内存映射与外设
内存映射,说白了就是给每个硬件模块分配一个地址范围。处理器通过访问这些地址来读写外设寄存器。我见过有人把外设寄存器当成普通内存来分析,结果死活看不懂数据变化。
| 地址范围 | 用途 | 说明 |
|---|---|---|
| 0x00000000 - 0x0FFFFFFF | DDR 内存 | 程序运行空间,可读写 |
| 0x10000000 - 0x1FFFFFFF | 外设寄存器 | UART、GPIO、Timer 等 |
| 0x20000000 - 0x2FFFFFFF | Flash 映射 | 只读,存放固件 |
| 0x30000000 - 0x3FFFFFFF | SRAM | 快速暂存区 |
逆向分析时,我通常会先找外设寄存器的基地址。比如 UART 的收发寄存器,如果看到代码在往某个固定地址写数据,那八成就是串口输出。我曾经靠这个技巧,在没有文档的情况下逆向出了一个路由器的调试接口。
MMIO 类型。这样反汇编时,对外设的读写操作会高亮显示,方便快速定位驱动代码。
2.7 实战:从固件中提取内存映射信息
拿到一个固件文件,怎么知道它的内存映射?我一般这么做:
- 用
binwalk扫描固件,看有没有文件系统。 - 解压出内核或 Bootloader,用
readelf -l查看段信息。 - 搜索字符串,比如
U-Boot、Barebox、DDR、base address。 - 如果找到 Bootloader 源码,直接看
include/configs/目录下的头文件。
# 示例:从 U-Boot 镜像中提取内存配置
strings u-boot.bin | grep -i "dram"
# 输出可能类似:
# DRAM: 128 MiB
# dram base: 0x80000000
# dram size: 0x08000000