0 前言#
路由器和NAS或许是这个时代的DIYer最爱折腾的两种设备了。然而我总觉得花大把时间调试这些设备,并不会给我的生产力带来多大的提升,因此兴趣寥寥。
然而一次路由器故障让我较起真来了:路由器的官方固件实在是太封闭了……我甚至看不到系统日志!为了排查故障原因,我开始尝试在路由器中刷入OpenWRT固件,获得完整的系统控制和日志功能。
寝室的设备是小米路由器4A千兆版 v2。相关的刷机教程实在是太多了,电脑小白照着做都能搞定。因此这篇文章将会分享我更加感兴趣的内容:嵌入式设备的一些原理,以及“为什么”要按照教程所说的进行操作。
提醒:本文仅为对个人尝试的记录。请自行承担参照本文操作所造成的后果。
1 原理科普#
最常见的嵌入式设备#
嵌入式这个名词,计算机专业的同学应该都不陌生。路由器就是我们生活中最常见的一类嵌入式设备:能够独立完成一些功能(组网、转发等),且具有【针对其功能高度定制】的软硬件——相当于一台精简特化版的计算机。
因此 CPU、内存等常见概念对路由器也适用。例如,我手上的这台路由器使用的就是使用的 MediaTek MT7621(基于 MIPS 的双核路由 SoC),采用 MIPS 架构,拥有 128MB 的 DDR3 内存。
与通用型计算机不同的是,路由器等嵌入式设备的系统(固件)通常无法随意更改,而是以映像的形式写入到 Flash 闪存芯片中的。而为了节省成本,闪存会被精细地分区,按用途划分地址区间。
这也是我们开始前需要认识到的一个关键:我们的操作从本质上来说是在闪存的特定偏移写入特定映像(固件),而不是简单的文件拷贝。
Bootloader:引导加载程序#
而在固件中,通常会有一个相对独立的部分,专门负责初始化硬件,并引导启动整个系统——我们称之为引导加载程序(Bootloader)。除此之外,它也会承担一些底层功能,例如进入恢复模式,便于开发者修改被引导的操作系统/应用程序。
很多案例中提到的设备刷机失败后“变砖”,多半就是因为 Bootloader 被破坏或者无法成功引导系统启动。而为了避免这一情况,大多数 DIYer 在对路由器进行刷机操作时,会首先刷入一个功能更加强大的 Bootloader——Breed。
Breed 的全称是 Boot and Recovery Environment for Embedded Devices——看名字也知道,它能够为嵌入式设备提供一个还原环境。Breed 在保持自身(Bootloader)不被破坏的情况下,支持可视化固件刷写等一系列功能。因其友好的界面和强大的功能为开发者和 DIYer 们提供了极大的便利,大家称它为“不死固件”。Breed 由 hackpascal ↗ 大佬开发,致敬。
(注:“不死”是一个戏称。刷写 Bootloader 风险极高,若不了解串口/硬件恢复流程或没有备份,不建议盲目替换。)

2 刷机实战#
操作方案#
在参考多份教程后,我最终参照了 获取原系统Root Shell → 刷入 Breed → 刷入 OpenWRT 的方案。接下来将对方案进行简述,并给出参考的教程。
获取原系统 Root Shell 考虑到安全性,原版系统一般会禁止用户刷入第三方固件。因此需要使用特殊手段,绕过这一限制。好在各路 DIY 大神早已给出了解决方案:执行脚本利用特定的漏洞,取得 Root 权限并开启 shell。脚本是用 Python 写的,环境配置起来很简单。如果一遍运行失败了,不妨试着再运行一次 ↗。
- 漏洞利用脚本开源仓库与文档(遇到问题时强烈建议优先查看 Issues):https://github.com/acecilia/OpenWRTInvasion ↗
- 操作教程&针对 4A千兆版v2 优化过的漏洞利用脚本:https://www.right.com.cn/forum/thread-8279698-1-1.html ↗

刷入Breed
脚本运行成功后,我们可以在 Shell 中使用 mtd 指令,将 Breed 写到 Bootloader 区块。当然在这之前,我们需要使用 dd 命令备份关键分区,包括无线模块校准数据等(后续还会用到)。在本地计算机使用 ftp 命令或者 WinSCP 等工具可以很方便地进行文件上传与下载。
- Breed 刷写傻瓜教程(不建议盲目照搬,进行每一步操作前想清楚):【教程】傻瓜版小米路由器开启telnet刷breed - 恩山无线论坛 ↗
- Breed 固件官网下载(需要下载对应版本,上述教程中也有附带):Boot and Recovery Environment for Embedded Devices (BREED) ↗
- 完整刷机方案教程(较简略,优点是流程齐全):小米路由器4A千兆版第二代(R4Av2)刷机教程 - CSDN ↗
刷入OpenWRT
在刷入 Breed 后,我们可以通过启动时按下 Reset 键,然后在电脑上访问 192.168.1.1,进入到上图所示的恢复控制台,并在控制台安装准备好的 OpenWRT 固件。需要注意的是,部分小米路由器的闪存布局不同,因此不能直接在 Web 界面刷入固件,需要使用 telnet 连接终端手动操作。后文也将对此问题进行详细说明。
- OpenWRT 固件安装教程:小米路由器 4A千兆版 V2 新版硬件安装OpenWRT - 知乎 ↗
- OpenWRT 官网的硬件配置信息与固件下载:Techdata: Xiaomi Mi Router 4A (MIR4A) Gbit v2 ↗
- 因闪存布局导致刷机问题的 issue-1:终于解决了breed刷机后不能正常启动的问题 - 恩山无线论坛 ↗
- 因闪存布局导致刷机问题的 issue-2:关于R4A(Breed)刷完Openwrt 无限重启问题 - 恩山无线论坛 ↗
开启OpenWRT之旅 成功刷入OpenWRT之后,可玩性就很高了。可以安装的应用非常丰富,包括透明代理,SmartDNS等功能。我们甚至可以自行编译OpenWRT固件,加上一些自定义的内核模块,实现更加底层且强大的功能。
- 后续自行编译 OpenWRT:小米路由器 4A千兆版 V2 新版硬件编译OpenWRT ↗
- 同型号自行编译固件的案例:Xiaomi_R4AGv2_OpenWrt自编译固件-防校园网检测 ↗
- OpenWRT 安装中文语言包:网工手艺-软路由OpenWRT(第2节,离线安装中文包) ↗
过程补充与原理解析#
尽管方案十分清晰,实操过程还是屡屡碰壁——但也因此探索到了不少非常有趣的知识。接下来,我将对上述教程中没有详细说明的部分进行补充与原理解析。
路由器中的闪存布局是怎样的?
前文中有提到,闪存芯片的空间有着严格的规划。在 Linux 系统下,闪存芯片被识别为 MTD(Memory Technology Device)设备,通过字符设备接口在 /dev/mtdX 和 /dev/mtdXro(只读)中表示。此外,也可以将一块闪存芯片将划分多个 MTD 设备,便于分区进行读写。
例如,在小米原版固件的 shell 下,使用命令 cat /proc/mtd,即可看到 MTD 设备及其分区情况(不同厂商/型号会有差异,请以自己设备的实际输出为准)。
root@XiaoQiang:~# cat /proc/mtd
dev: size erasesize name
mtd0: 01000000 00010000 "ALL"
mtd1: 00030000 00010000 "Bootloader"
mtd2: 00010000 00010000 "KF"
mtd3: 00010000 00010000 "Bdata"
mtd4: 00010000 00010000 "Factory"
mtd5: 00010000 00010000 "crash"
mtd6: 00010000 00010000 "cfg_bak"
mtd7: 00100000 00010000 "overlay"
mtd8: 00e70000 00010000 "OS1"
mtd9: 001a0000 00010000 "kernel"
mtd10: 00cd0000 00010000 "rootfs"
mtd11: 00010000 00010000 "Config"shell可以发现,mtd0 设备对应的是整个闪存芯片,大小为0x0100000 字节,转换为十进制可得知其大小恰好为 16MB。不过该命令只显示了各个分区的大小,不同分区的地址仍需要手动计算。但我们可以在内核消息中看到更详细的地址范围,也就是闪存布局。使用 dmesg 命令打印,可以看到:
[ 1.022063] Creating 10 MTD partitions on "spi32766.0":
[ 1.027304] 0x000000000000-0x000001000000 : "ALL"
[ 1.033752] 0x000000000000-0x000000030000 : "Bootloader"
[ 1.040716] 0x000000030000-0x000000040000 : "KF"
[ 1.046888] 0x000000040000-0x000000050000 : "Bdata"
[ 1.053450] 0x000000050000-0x000000060000 : "Factory"
[ 1.060173] 0x000000060000-0x000000070000 : "crash"
[ 1.066656] 0x000000070000-0x000000080000 : "cfg_bak"
[ 1.073364] 0x000000080000-0x000000180000 : "overlay"
[ 1.080065] 0x000000180000-0x000000ff0000 : "OS1"
[ 1.092973] 2 fit-fw partitions found on MTD device OS1
[ 1.098220] 0x000000180000-0x000000320000 : "kernel"
[ 1.104848] 0x000000320000-0x000000ff0000 : "rootfs"plaintext从上文的信息中可以解读出我们在刷机过程中需要掌握的关键信息:
0x000000-0x030000:Bootloader存储的位置,长度为192KB;0x040000-0x050000:Bdata数据,保存了设备的出厂参数,包括序列号,区域码等数据;0x050000-0x060000:Factory数据,存储 Wi-Fi 射频校准参数、MAC 地址等关键数据。也就是上述教程中所说的“EEPROM”数据。0x180000-0xff0000:固件存储区域。
而我们的刷机过程,实际上就是将 Breed 固件写入到 0x000000-0x030000 区域,再将OpenWRT 固件写入到 0x180000 地址往后的区域。此外,为了保证 Wi-Fi 模块正常工作,我们需要备份 Factory 区块的数据,并在刷入 OpenWRT 后还原。
(注:OpenWrt 社区和多起 issue 表明:如果 factory/bdata 分区被覆盖或丢失,可能会导致无线功能不可用。刷机务必备份这些分区并验证成功还原。)
为什么不能直接用 Breed 图形界面刷入 OpenWRT 固件?
不同设备的闪存布局各不相同,也就是说,它们的固件起始地址并不相同。这就会导致一个问题:如果没能使固件存储位置和闪存布局相匹配,bootloader将无法从正确的地址寻找到固件入口,从而无法引导系统启动,陷入启动→失败的死循环。
而在 Breed 中,默认的固件地址是 0x050000,也就是说 Breed 会尝试从该地址寻找固件程序的入口,在刷机时,也会将固件放置到这一位置。然而在这一型号的小米路由器中,0x180000 才是固件的起始地址——亦即固件认为自己应当被放到 0x180000 上。因此若直接在图形界面刷入公版 OpenWRT 固件,将导致无法启动。
而在一些网友自编译的 OpenWRT 固件中,固件中的配置被修改,把固件入口改到了 0x050000 处,和 Breed 适配。刷入这类固件则不会引发这类问题。同样,若是使用原版Bootloader(U-Boot),搭配公版 OpenWRT 固件,也不会导致这一问题。
因此,若想刷入公版 OpenWRT 固件,就需要在 Breed 命令行中,手动把固件拷贝到 0x180000 的位置,然后修改 Breed 的环境变量,使它启动时从对应的地址引导。
如何手动刷入固件与EEPROM?
上面的几篇教程在涉及该问题时,都没有说得太明白。因此我这里再进行一个补充。在 Breed 命令行中的操作流程包括:下载固件→擦除目标区域→刷写固件到目标区域。在这之后,对 Breed 的环境变量进行配置,修改引导地址即可。
下载固件到设备
Breed 命令行没有附带 FTP 功能,只提供了一条 wget 命令用于下载文件。因此,我们需要在操作的计算机上架设一个 HTTP 文件服务器,以便从 192.168.1.2 地址下载需要的文件。为此,我在本地运行了HFS ↗,一个开源的HTTP文件服务器工具。

接着,在命令行使用wget获取固件,后接的链接即文件在 HFS 上的位置。传输完成后,可以看到固件被保存到了 0x80001000 地址。

擦除并将固件刷写到目标区域
使用 flash 命令可以对闪存进行擦写操作。指令格式如下所示:
# 擦除:起始偏移 长度(按 0x10000 或设备擦除块对齐)
flash erase 0x00180000 0x00e80000
# 写入:目标偏移 源内存地址 文件长度
flash write 0x00180000 0x80001000 0x00e00000shell我们只需对目标地址 0x180000 执行一次擦除操作,再将固件从 0x80001000 写入到目标地址即可。注意操作时的长度。擦除长度大于文件写入长度即可。文件长度可以在上一步的传输结果中获取。在完成这一步操作后,可以尝试使用 boot 指令引导固件。若引导成功,则说明固件刷写成功。

EEPROM文件的刷写操作同理,只需修改拷贝到的地址就可以了。
修改Breed环境变量 这一步比较简单,只需再次进入 Breed Web 恢复控制台,修改环境变量即可。保存后重启路由器。若成功引导进入 OpenWRT ,则说明操作成功。

3 结语#
通过探索路由器设备的固件更新过程,我对嵌入式、Linux 等多个概念有了更深的认知。没想到课程中学习的嵌入式和计算机组成原理知识在这里派上了用场。
在给路由器更新系统之后,我成功定位了问题所在,并一定程度上修复了这一问题。之后我也许会再写一篇文章,说说我的排障思路。下一步,我打算在 OpenWRT 中安装 SmartDNS 等程序(之前用的是树莓派),改善上网体验。
不得不说,折腾这类设备确实有它的乐趣所在。但比起“部署成功”给我带来的成就感,摸清楚教程步骤背后的“为什么”对我来说更有魅力。
的确,对设备进行一些 DIY 看起来酷毙了。但如果这种 DIY 背后蕴含的知识、底层原理,以及自己动手探索的极客精神,变成了烂大街的傻瓜教程和不加思考的模仿——我想这是得不偿失的。