升级命令到底写在哪个字节?——misc分区逐段拆解
设备开机进不进 recovery,其实不看代码看 misc 分区那 16K 里写了什么。u-boot 上电就读它:command 字段是 boot-recovery 就进 recovery,空着就正常启动。这一篇把 misc 逐段拆开,最后再和官方文档对一遍——有三处对不上。
问题场景
update ota /udisk/update.img 敲下去,设备重启就进了 recovery 开始刷机。但这条命令并不是直接交给 recovery 的——它先被写进 misc 分区,重启后由 u-boot 读出来,再决定引导哪个系统。
于是几个问题就冒出来了:misc 里到底哪个字节决定"进 recovery"?要升级哪些分区的掩码写在哪?为什么 mk-misc.sh 里那条 dd 命令的偏移要写成 16*1024+64,这个 64 是谁定的?
一、misc 分区为什么存在
misc 是 miscellaneous 的缩写,杂项、大杂烩。这个概念是从 Android 那边继承过来的:它是一块没有文件系统的裸分区,专门用来在"当前系统"和"引导程序/升级程序"之间传话。
它解决的是一个很具体的时序问题——想升级系统,就得先把系统停掉,可系统停了之后谁来执行升级?
三方都不需要同时在线:Normal 系统只管写,U-Boot 只管读,Recovery 最后负责清。掉电了也不要紧,因为命令还躺在 misc 里,下次上电 u-boot 照样能读到。
misc 分区在分区表里其实挺大。看 RK3576 的 parameter.txt:
CMDLINE: mtdparts=:0x00002000@0x00004000(uboot),0x00002000@0x00006000(misc),...
mtdparts 的写法是 大小@偏移(名字),单位是 512 字节的扇区。misc 分到 0x2000 个扇区,也就是4 MiB——但真正用到的只有开头那48 KB,后面全是浪费。下面这段脚本能说明问题:
# device/rockchip/common/scripts/mk-misc.sh# The old windows tools don't accept misc > 64Ktruncate -s 48k "$MISC_IMG"
镜像只做 48K,因为老的 Windows 打包工具不接受超过 64K 的 misc。分区大、镜像小,这是历史包袱留下的错位。
二、48K 里分四段
官方《Rockchip Linux updateEngine 升级方案介绍》8.2 节给了一张三行的表。加上代码里实际用到但文档没写的一段,真实布局是这样的:
| 偏移 | 长度 | 用途 | 定义位置 |
|---|---|---|---|
| 2 K (0x800) | 32 B | Linux A/B 引导信息 AvbABData | MISC_OFFSET |
| 6 K (0x1800) | 17 B | 格式化命令 cmd_wipe_userdata | MISC_OFFSET_CMDLINE |
| 10 K (0x2800) | 1 K | 自定义命令(长度前缀 + 内容) | MISC_OFFSET_CUSTOM |
| 16 K (0x4000) | 1088 B | bootloader_message,Recovery 与 Normal 通信 | BOOTLOADER_MESSAGE_OFFSET_IN_MISC |
对应 bootloader.h:22-25:
static const int BOOTLOADER_MESSAGE_OFFSET_IN_MISC = 16 * 1024;#defineMISC_OFFSET 2048 //A,B 结构体在偏移2K 的位置#defineMISC_OFFSET_CMDLINE 6144 //擦除命令,在偏移4K 的位置,配合挂载脚本使用#defineMISC_OFFSET_CUSTOM (10 * 1024) //CUSTOM, 8K (10K), length 1K
先记住这三行的注释——有两行是错的:MISC_OFFSET_CMDLINE 的值是 6144(6 K)而注释写"偏移4K",MISC_OFFSET_CUSTOM 的值是 10 * 1024(10 K)而注释写"8K (10K)",一句注释里自己就自相矛盾。第七节会拿官方文档再对一遍。
四段之间互不干扰,因为它们由不同的模块读写:2K 归 A/B 槽位管理,6K 归格式化,16K 归 recovery 命令。这也是为什么它们能挤在一块裸分区里而不打架。
三、16K 处:recovery 命令长什么样
这是整个 misc 分区里最关键的结构。bootloader.h:69-75:
struct bootloader_message {char command[32];char status[32];char recovery[768];char needupdate[4];char systemFlag[252];};
逐字段算一下偏移,总数正好 1088 字节:
| 字段 | 偏移 | 长度 | 谁写 | 作用 |
|---|---|---|---|---|
| command | 0 | 32 | Normal / Recovery | boot-recovery 时进 recovery,全 0 时正常启动 |
| status | 32 | 32 | Bootloader | 引导程序回填的状态文本 |
| recovery | 64 | 768 | Normal / Recovery | 换行分隔的参数列表,第一行必须是 recovery |
| needupdate | 832 | 4 | Normal | 按 int 读 ,就是升级分区掩码 |
| systemFlag | 836 | 252 | Recovery | 升级成功后记下升级包路径 |
needupdate 只有 4 个字节,看着不起眼,但它是 Normal 系统和 Recovery 之间传递"要升级哪些分区"的唯一通道。读法很直接,rkupdate.c:151-153:
struct bootloader_message boot;get_bootloader_message(&boot);sprintf(args[4], "--partition=0x%X", *((int *)(boot.needupdate)));
把 needupdate 那 4 个字节当成一个 int 读出来,拼成 --partition=0x3FFC00 这样的参数传给升级程序。这一串位掩码怎么解读,是下一篇的事。
command 和 recovery 的分工
容易混淆的是这两个字段。command 是给 u-boot 看的开关,recovery 是给 recovery 程序看的参数。u-boot 只判断前者:
// u-boot/arch/arm/mach-rockchip/spl_boot_mode.crecovery = !strcmp(bmsg->command, "boot-recovery");
一个字符串比较,就决定了这次上电走哪条路。而 recovery 启动后,会拿 recovery 字段做参数解析——第一行必须是字面量 recovery,后面每行一个参数:
command = "boot-recovery"recovery = "recoveryn--update_package=/udisk/update.imgn"
这也就解释了为什么 mk-misc.sh 里写的是两条 dd,而且第二条要落在 +64:
# device/rockchip/common/scripts/mk-misc.shelif [ "$RK_MISC_RECOVERY" ]; thenecho -n "boot-recovery" |dd of="$MISC_IMG" bs=1 seek=$((16*1024)) conv=notruncecho -e -n "recoveryn$RK_MISC_RECOVERY_ARG" |dd of="$MISC_IMG" bs=1 seek=$((16*1024+64)) conv=notruncnotice "Generated recovery misc with: $RK_MISC_RECOVERY_ARG"
16*1024 是 command 的偏移,16*1024+64 是 recovery 的偏移——那个 64 就是 command[32] + status[32]。脚本作者是把结构体偏移手算出来写死在 shell 里的。
这里有个文档和代码对不上的地方值得说清楚。
官方文档 4.1.1 节写的是"默认使用 wipe_all-misc.img 作为 misc 分区的固件"。但这份 SDK 里的 choice没有写 default:
choiceprompt "misc image"config RK_MISC_BLANK ← 第一项config RK_MISC_RECOVERYconfig RK_MISC_CUSTOM
kconfig 在没有 default 时取第一个选项,也就是 RK_MISC_BLANK。实际生成的配置也确实是:
# output/.configRK_MISC=yRK_MISC_BLANK=y# RK_MISC_RECOVERY is not set
所以这份 SDK 默认生成的是空白 misc,开机直接进正常系统,不会自动擦数据。可以直接验证——output/misc.img 在 16384 偏移处是整片 00:
xxd -s 16384 -l 32 output/misc.img# 00004000: 0000 0000 0000 0000 0000 0000 0000 0000
想让设备上电进 recovery 擦一次,需要主动选中RK_MISC_RECOVERY:
config RK_MISC_BLANK # 空白的config RK_MISC_RECOVERY # 带 boot-recovery 命令的config RK_MISC_CUSTOM # 用 chip 目录下的自定义镜像
选中之后,写入的命令行内容由 RK_MISC_RECOVERY_ARG 决定,它的默认值是 --wipe_all:
config RK_MISC_RECOVERY_ARGstring "recovery argument"depends on RK_MISC_RECOVERYdefault "--wipe_all"
也就是说:默认配置不会自动擦数据;只有主动选了 recovery misc,才会以 --wipe_all 进入擦除流程。这和文档给人的印象正好相反。
四、2K 处:A/B 模式的槽位信息
如果用了 Linux A/B 双系统,2K 偏移处还躺着 32 字节的槽位元数据:
typedef struct AvbABData {unsigned char magic[AVB_AB_MAGIC_LEN]; // "�AB0"unsigned char version_major;unsigned char version_minor;unsigned char reserved1[2];AvbABSlotData slots[2]; // 每个 4 字节unsigned char last_boot;unsigned char reserved2[11];unsigned int crc32; // 覆盖前面 28 字节} AvbABData;
每个 AvbABSlotData 是 4 个字节,值的合法范围代码里写得很清楚:priority 取 0~15(0 表示不可引导,15 最高),tries_remaining 最多 7 次。
CRC 的算法有个细节值得注意——它和 AVB 标准对齐,所以是一次大端的 CRC32:
static void AvbABData_update_crc(struct AvbABData *data){data->crc32 = avb_htobe32(avb_crc32((const unsigned char*)data,sizeof(struct AvbABData) - sizeof(unsigned int)));}
- sizeof(unsigned int) 就是那 28 字节。校验范围不含 crc32 自己,这是校验和的标准做法。
A/B 那套切换逻辑(setSlotSucceed / setSlotActivity)留到后面单独一篇,这里只需要知道:2K 是 A/B 的地盘,和 recovery 命令互不相干。
五、6K 和 10K:两个冷门段落
6K 处用于格式化命令。rkbootloader.c:430-446 里 wipe_userdata() 会根据当前模式分两路:
int wipe_userdata(int auto_reboot){int slot = -1;slot = getCurrentSlot();if (-1 == slot) {// 写MISC(源码原注释)struct bootloader_message msg;memset(&msg, 0, sizeof(msg));char recovery_str[] = "recoveryn--wipe_all";strcpy(msg.command, "boot-recovery");sprintf(msg.recovery, "%sn", recovery_str);set_bootloader_message(&msg);return 0;}if (writeCmdMisc((char *)CMD_WIPE_USERDATA, sizeof(CMD_WIPE_USERDATA)) != 0) {return -1;}...}
这个分叉是这样的:getCurrentSlot() 返回 -1 表示当前没在 A/B 模式下跑(读 /proc/cmdline 里的 androidboot.slot_suffix,实现见 update_engine/rktools.c,12 篇会展开),这时走 BCB——让设备重启进 recovery 去擦;否则只往 6 K 写一条命令。
需要说明的是,本 SDK 里我没找到读这个 6 K 命令的地方。readCmdMisc() 只被 miscDisplay() 调用过(也就是敲 --misc=display 时打印出来),其余没有任何脚本或程序去读它。所以 A/B 模式下这条命令写下去之后由谁执行,至少在 external/recovery 这份代码里是看不到的——用之前务必实测确认。
而单系统模式这条 BCB 通路是确定没问题的,u-boot 会读、recovery 会执行。
10K 处是自定义命令区,格式是"2 字节长度 + 内容",上限 1022 字节。updateEngine 提供 --misc_custom=read|write|clean 三个操作来读写它,内容会落到 /tmp/custom_cmdline 这个临时文件。
六、两种存储介质,两套读写代码
misc 的读写入口都带一层分发,bootloader.c:35-49:
int get_bootloader_message(struct bootloader_message *out){if (isMtdDevice())return get_bootloader_message_mtd(out);return get_bootloader_message_block(out);}
块设备(eMMC/SD)简单粗暴——直接当成普通文件,fseek 到 16K 读写 1088 字节:
fseek(f, BOOTLOADER_MESSAGE_OFFSET_IN_MISC, SEEK_SET);int count = fread(&temp, sizeof(temp), 1, f);
MTD(NAND)就没这么自由了,因为裸 NAND 只能按页读写,而且有坏块。所以代码的策略是读-改-写:开一块 20K 的缓冲区、整块读出来、改中间那一小段、再整块写回去:
#defineMISC_SIZE ((16 + 4) << 10) // 20K#defineCMD_OFFSET (16 << 10) // 16Kchar data[MISC_SIZE];const ssize_t size = (MISC_SIZE / write_size) * write_size;mtd_read_data(read, data, size);memcpy(&data[CMD_OFFSET], in, sizeof(*in)); // 只改这一小段mtd_write_data(write, data, size); // 整体写回
顺带解释一下那两个数字:缓冲区取20K,因为 16K 偏移 + 1088 字节的结构体 放得下,留了 4K 余量;而 write_size 是擦除块大小,实际读写长度是 20K 按擦除块向下取整——不是 20K 整。这也是为什么上面那行除法要先除再乘:只读写整块的倍数,多出来的零头不碰。
另外 isMtdDevice() 也不是猜的,它去 /proc/cmdline 里翻 storagemedia= 字段:
// external/recovery/rktools.c// storagemedia=mtd → MTD 设备// storagemedia=sd → 还要再看 /proc/mtd 里是否真有 mtd 节点// 其余 → 按块设备处理
所以同一份 recovery 二进制能同时跑在 NAND 和 eMMC 板子上。
七、和官方文档对一遍:三处对不上
官方文档 8.2 节那张表是三行:2k / 4k / 16k。拿它和代码逐个核对,能挑出三个问题。
第一处:4K 还是 6K。文档说格式化命令在 4k,代码里 MISC_OFFSET_CMDLINE 却是6144,也就是 6K:
#defineMISC_OFFSET_CMDLINE 6144 //擦除命令,在偏移4K 的位置,配合挂载脚本使用
注意注释自己也写错了——值写 6144,注释写"偏移 4K"。文档多半是照着这句注释抄的。同一个文件里 2K 的 MISC_OFFSET 2048 用的是十进制,这里也用的是十进制,所以 6144 不是笔误,是真的按 6K 算的。
顺带说一句,我在本 SDK 里没有找到读这个位置的地方。官方文档说由 S21mountall.sh 识别并格式化 userdata,但这个 SDK 的 S00mountall.sh 只有一句 mount-helper,不碰 misc。如果要依赖 6K 这条通道,建议先在板子上实测一遍再上生产。
第二处:10K 那段文档没写。MISC_OFFSET_CUSTOM 占用 10K 起 1KB,是 updateEngine --misc_custom 的落脚点,官方 8.2 的三行表里完全没有它。
第三处:命令文件的位置。这条不在 8.2 节,但同一个坑——recovery.c 开头那段 70 多行的注释(从 AOSP 继承来的)一直在说 /cache/recovery/command,包括"FACTORY RESET 流程"那几段伪代码。但代码里的常量是:
static const char *COMMAND_FILE = "/userdata/recovery/command";static const char *LOG_FILE = "/userdata/recovery/log";
没有 cache 分区,全都落在 userdata。读那段注释会把人带偏,下一篇讲到 recovery 取参数时会再遇到它。
八、自己动手看一眼
想确认板子上 misc 到底是什么内容,不用烧写工具,直接读:
# 看 2K 处的 A/B 信息、6K 处的格式化命令、16K 处的 recovery 命令updateEngine --misc=display# 或者直接用 dd 把 16K 起 1088 字节抠出来dd if=/dev/block/by-name/misc of=/tmp/bcb.bin bs=1 skip=16384 count=1088strings /tmp/bcb.bin
--misc=display 背后就是 rkbootloader.c:576-611 的 miscDisplay(),它把三段内容连同"现算的 CRC"和"本地存的 CRC"一起打印出来,扫一眼两个值对不对得上——是排查 A/B 问题的第一手工具。
本文 zblog模板 原创,转载保留链接!网址:https://www.wbaas.cn/fengrong/2431.html
1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

