14 KiB
本文是当时的分析 / 交付存档,路径按当时的工作目录写。 本项目里的对应关系:tools 对应 _tools,inputs/target/vela_app.bin 对应 _x/vela_app.bin, inputs/source 对应 _s5,.work 对应 _out(产物),dist 对应 _out/faces。
Xiaomi Watch S4 eSIM(o62lte)系统应用图标替换 —— 固件包分析报告
分析对象:c53475a6c7367c3583c6882665fd85ae_upd_mijia.watch.o62lte.bin
固件标识:ro.build.version=3.202.79, build.time=2026-07-03 19:58:07, model=o62lte
机型字符串:Xiaomi Watch S4 eSIM
包大小:199,385,001 字节
1. 固件包是什么
外层就是一个 ZIP(Android SignApk 签名包),开头即 PK\x03\x04,第一个条目是 ota.sh。
ota.sh 4524
vela_app.bin 265684992 <- 应用资源分区(图标在这里)
vela_watchface.bin 92967936
vela_system.bin 72366080
vela_font.bin 36146176
vela_misc.bin 35799040
vela_vendor.bin 17909760
vela_ap.bin 11141120
vela_quickapp.bin 5840896
vela_i18n.bin 4399104
vela_cp.bin 3174400
vela_audio.bin 2730024
vela_ota.bin 1441792
vela_tee.bin 1048576
META-INF/CERT.RSA / CERT.SF / MANIFEST.MF
ota.sh 的全部动作就是逐分区裸写:
dd if=/ota/vela_app.bin of=/dev/app bs=32768 verify
dd if=/ota/vela_vendor.bin of=/dev/vendor bs=32768 verify
dd if=/ota/vela_system.bin of=/dev/system bs=32768 verify
... (vendor/i18n/app/quickapp/cp/misc/audio/tee/system/font/ap/watchface)
签名信息:CERT.SF 里 Created-By: 1.0 (Android SignApk)、X-Android-APK-Signed: 2, 3,MANIFEST.MF 对每个分区镜像记录 SHA1。
结论:这份 OTA 包无法自行重新打包。 改一个字节就过不了签名校验。
vela_*.bin 是 Linux romfs 镜像(magic -rom1fs-):
| 镜像 | 卷名 | 条目数 | romfs 大小 |
|---|---|---|---|
| vela_app.bin | app | 7834 | 265,684,928 |
| vela_watchface.bin | watchface | 312 | 92,967,424 |
| vela_system.bin | system | 1525 | 72,365,728 |
| vela_font.bin | font | 15 | 36,145,776 |
| vela_misc.bin | mcmisc | 1341 | 35,798,176 |
| vela_vendor.bin | vendor | 58 | 17,909,328 |
| vela_quickapp.bin | quickapp | 46 | 5,840,544 |
| vela_i18n.bin | i18n | 277 | 4,399,072 |
在盘上它们挂到 /resource/<卷名>,所以 AP 二进制里出现的是 /resource/app/... 这样的绝对路径。
ROMFS 结构(关键)
super : "-rom1fs-" | u32 size | u32 checksum | name(NUL, 补齐16)
inode : u32 next | u32 spec | u32 size | u32 checksum | name(NUL, 补齐16) | 文件数据紧跟其后
next 低 4 位 = 类型:0=硬链接(spec=目标inode偏移) 1=目录(spec=首个子项偏移) 2=普通文件 8=可执行
目录项与文件数据是严格顺序紧密排布的,每一项的偏移完全由它前面所有内容的长度决定。
2. 系统自带应用的图标在哪里(已定位)
AP 二进制里写死了一批路径,直接给出答案:
/resource/app/alarm/launcher.bin
/resource/app/activities/launcher.bin
/resource/app/calendar/launcher.bin
/resource/app/heartrate/launcher.bin
... 共 39 个
精确统计:38 个 <app>/launcher.bin 加 1 个 phone/contacts_launcher.bin(联系人图标),共 39 个。
即 每个系统应用目录下都有一个 launcher.bin,它就是"应用列表/启动器"里显示的那个图标。全部位于 vela_app.bin(=/dev/app 分区,265 MB)。
39 个图标全部是 LVGL v9 二进制图片:
u8 magic=0x19 | u8 color_format | u16 flags | u16 w | u16 h | u16 stride | u16 reserved
cf=0x10 ARGB8888 : 12 + 144*144*4 = 82956 字节
cf=0x0a I8 : 12 + 256*4 + 144*144 = 21772 字节
已全部解码验证(38/39,voice/alexa/launcher.bin 是 6005 字节的特例,flags=0x0008,格式待定)。
图标清单(偏移为 vela_app.bin 内绝对偏移,固件 3.202.79 专用)
| 应用 | 数据偏移 | 长度 | 尺寸 | 格式 |
|---|---|---|---|---|
| wxpay | 0x0000bc00 | 21772 | 144x144 | I8 |
| womenhealth | 0x00047a00 | 82956 | 144x144 | ARGB8888 |
| weather | 0x017de200 | 82956 | 144x144 | ARGB8888 |
| walkie_talkie | 0x02d45600 | 82956 | 144x144 | ARGB8888 |
| voice/alexa | 0x032fa000 | 6005 | 144x144 | I8(特例) |
| voice/aivs | 0x0341c400 | 82956 | 144x144 | ARGB8888 |
| todolist | 0x03aa9000 | 82956 | 144x144 | ARGB8888 |
| timer | 0x03ad3c00 | 82956 | 144x144 | ARGB8888 |
| sports/training | 0x0466b000 | 82956 | 144x144 | ARGB8888 |
| sports/record | 0x04a63000 | 82956 | 144x144 | ARGB8888 |
| sports | 0x04baee00 | 82956 | 144x144 | ARGB8888 |
| sports/course | 0x05132400 | 82956 | 144x144 | ARGB8888 |
| sleep | 0x05156400 | 82956 | 144x144 | ARGB8888 |
| settings | 0x052e8000 | 82956 | 144x144 | ARGB8888 |
| remote_camera | 0x06593a00 | 21772 | 144x144 | I8 |
| recorder | 0x067a4600 | 82956 | 144x144 | ARGB8888 |
| pressure | 0x07908400 | 82956 | 144x144 | ARGB8888 |
| phone | 0x07a4d000 | 82956 | 144x144 | ARGB8888 |
| phone/contacts_launcher | 0x07a82200 | 82956 | 144x144 | ARGB8888 |
| perpetual_calendar | 0x07b3be00 | 21772 | 144x144 | I8 |
| oxygen | 0x08f08c00 | 82956 | 144x144 | ARGB8888 |
| nfccard | 0x093f2c00 | 82956 | 144x144 | ARGB8888 |
| mijia | 0x09599200 | 82956 | 144x144 | ARGB8888 |
| media | 0x0968e200 | 82956 | 144x144 | ARGB8888 |
| lua | 0x096b5200 | 21772 | 144x144 | I8 |
| interconnect | 0x096d8000 | 82956 | 144x144 | ARGB8888 |
| innovation_research | 0x096efa00 | 82956 | 144x144 | ARGB8888 |
| heartrate | 0x09fea800 | 82956 | 144x144 | ARGB8888 |
| health_today | 0x0a223a00 | 82956 | 144x144 | ARGB8888 |
| flashlight | 0x0b6dba00 | 82956 | 144x144 | ARGB8888 |
| find_phone | 0x0b6f0000 | 82956 | 144x144 | ARGB8888 |
| esimsms | 0x0b762200 | 82956 | 144x144 | ARGB8888 |
| debug | 0x0b81a200 | 21772 | 144x144 | I8 |
| compass | 0x0b8a1400 | 21772 | 144x144 | I8 |
| chronograph | 0x0ba47e00 | 82956 | 144x144 | ARGB8888 |
| calendar | 0x0bc4b200 | 82956 | 144x144 | ARGB8888 |
| breath | 0x0bd54c00 | 82956 | 144x144 | ARGB8888 |
| barometer | 0x0e825c00 | 82956 | 144x144 | ARGB8888 |
| alarm | 0x0f3a7e00 | 82956 | 144x144 | ARGB8888 |
3. 疑问一:为什么"一个 Lua 表盘点一下就替换成功了"
从这份固件包里能确证的事实:
- 系统应用图标 =
vela_app.bin里的launcher.bin,而vela_app.bin是整个 265 MB 分区镜像; - 官方通道
ota.sh的写入方式是dd ... of=/dev/app,裸写分区; - 官方 OTA 包是 SignApk v2/v3 签名的,因此不可能通过"重新打包 OTA"来替换图标;
- 分区设备节点
/dev/app、/dev/system等对用户态是可打开的字符/块设备(LoXe 文档也专门警告"不要直接试写未知设备,对分区节点dd/重定向可能立即破坏系统")。
把这四点连起来,社区图标包只能走一条路:绕过签名校验,直接把图标字节写进分区。
所以那个"Lua 表盘"的角色是运载 + 执行载体,而不是"表盘 API 能设置图标":
图标包(做成表盘包的样子)
├── asset/*.bin 实际是一个"补丁表":[(绝对偏移, 该长度字节), ...]
└── main.lua ui.init / 按钮回调里做写入
│ 点击"立即替换"
▼
按记录 seek+write 到 /dev/app(或整镜像 dd)
▼
ROMFS 是只读挂载 + 有页缓存 → 必须重启才能重新读取
▼
重启后应用列表图标变了
"稍等片刻 + 重启"正是这条链路的指纹:如果只是普通文件级改动,不需要重启;裸写分区必须重启,因为已经挂载的只读视图和缓存都是旧的。
这一节里,(1)(2)(3)(4) 是本次从固件包实证的;"表盘脚本直接写
/dev/app" 是最符合全部观察的推断,需要用那个图标包本身(看main.lua是否出现lvgl.fs.open_file+/dev/app+write+seek)或实机日志来最终确认。 另外一种可能是:表盘只是商店分类/容器格式,真正推送由工具箱的安装通道完成。区分方法同上——看包里有没有 Lua 写盘代码,以及 payload 是"几十 KB 的补丁表"还是"75 MB 的整分区镜像"。
4. 疑问二:为什么"只适用于特定固件版本"(例如 3.100.032)
先纠正一点:锁版本的不是工具,是包里的那份 payload。 AstroBox 这类工具箱本身跨版本通用;写着"仅适用于 3.100.032"的是那份资源包。
原因有四层,前两层是技术必然,后两层是工程现实:
① 绝对寻址 + 顺序排布(决定性原因)
ROMFS 把所有 inode、文件名和文件数据首尾相接顺序排布。补丁表里存的是绝对偏移(例如 0x0f3a7e00)。只要前面对任何一个文件的长度变了 —— 新增一个图标、换一个字体、改一句文案、某个 .bin 从 I8 变 ARGB8888(长度差 61184 字节)—— 后面所有偏移全部平移。此时旧补丁会把图标字节写进别的文件里,轻则图标错乱,重则损坏资源分区。
这跟"为什么这个 APK 版本的汉化补丁不能用到下一个版本"是同一类问题:不是逻辑变了,是逐字节布局变了。
② 一致性 资源分区与 AP/system 分区是配套发布的(资源清单、图标索引、字体/i18n 版本互相对应)。混搭会出现显示错乱,严重时起不来。所以包作者干脆要求"先降级到配套版本"。
③ 官方 OTA 会整分区覆盖
任何系统升级都执行 dd ... of=/dev/app,你的图标会被整体重写回去。所以"锁版本"同时意味着"OTA 之后失效";如果升级后还拿旧偏移去写,就是往错误位置写数据。
④ 作者的取材边界
那个包和它同一作者发布的小米手环11固件、红米手表6固件放在一起 —— 他是做固件 dump 的人,只为"手上有 dump 的那个版本"做偏移表。另外那份 S4 41mm 的包标的是 3.100.032,而你这台 eSIM 是 3.202.79,本来就不是同一条构建线。
所以"难道每个版本都有不可替代的变化吗"——不是。 是"按绝对偏移硬写"这个做法天生不能跨版本。换一种做法就可以跨版本(见下一节)。
5. 更好的做法:不重建 ROMFS,按名字原地替换(已实现并验证)
因为 144×144 的图标换成另一个 144×144、同色彩格式的图标时,字节长度完全相同,所以可以:
在设备上解析 ROMFS → 按路径找到
<app>/launcher.bin→ 用等长的新图标覆盖它的数据 → inode 表、super block、后续所有偏移全部不动。
这样:
- 不需要重建 ROMFS,不需要改
size/checksum/next; - 不需要"每版本一份包"——偏移是运行时从当前固件里算出来的,所以天然跨固件版本;
- 传输量从"265 MB(或压缩 75 MB)整分区"降到"每个图标 83 KB / 22 KB"。
已验证的实现
工具:tools/romfs.py(ROMFS 解析/遍历)、tools/iconpack.py(make / apply / list)、tools/render_icons.py(解码为 PNG)。
验证结果:
| 验证项 | 结果 |
|---|---|
| ARGB8888 编码器与原固件字节级一致(alarm 图标往返) | ✅ 完全一致(82956 字节) |
| I8 编码器长度一致 + 视觉误差 | ✅ 长度一致,平均绝对误差 0.22/255 |
| 原地替换后条目数 / 路径 / inode 表 / super block / 文件总长 | ✅ 全部逐位不变(7834 条,inode table identical: True) |
| 内容变化的文件 | ✅ 恰好只有被替换的那几个 launcher.bin |
用 4 个自制图标(alarm / timer / wxpay / settings)跑通了
make → apply → 校验 → 回读渲染全流程,见 _out/patched_custom.png。
6. 还缺的那一环:写入通道
到这里,"改什么、改成什么、怎么做到版本无关"都已经解决。唯一未解决的是"怎么把这几万字节送进 /dev/app":
| 方式 | 说明 | 代价 |
|---|---|---|
实机 shell dd |
最直接,需要能拿到 shell(或 Recovery) | 需要相应工具/权限 |
| 表盘包 + 脚本写分区节点 | 社区现有做法,整链路已由本次分析解释清楚 | 属于滥用未公开行为,OTA 后失效;写错偏移会损坏分区 |
| 重打包 OTA zip | ❌ 签名校验过不去 | 不可行 |
要在你的机器上落地,下一步需要的是:在真机上把 /dev/app(或至少 ROMFS 头与目标 inode)读出来,用 iconpack.py list 得到你这一版固件的真实偏移,再生成补丁。这样就不再受"包只适用于某某版本"的限制。
7. 风险提示
- 对分区节点写数据是文档明确警告的危险操作;写错偏移会破坏资源分区,可能导致无法开机。
- 写之前务必备份原始分区(至少备份 ROMFS 头 + inode 表 + 你要改的
launcher.bin原字节,能回滚)。 - 系统 OTA 会覆盖你的修改;跨版本套用旧补丁 = 往错误位置写数据。
- 修改系统资源分区可能影响保修;请自行确认设备所有权与当地法律。
voice/alexa/launcher.bin(6005 字节,flags=0x0008)格式尚未确定,不要动它。
附:工作目录
_x/ 解包后的固件(含 13 个分区镜像)
tools/romfs.py ROMFS 解析与遍历
tools/iconpack.py list / make / apply 图标补丁
tools/render_icons.py 把 launcher.bin 解码成 PNG
tools/patch_demo.py 原地等长替换的概念验证
tools/demo_icons.py 编码器自检 + 生成示例图标
tools/verify_patch.py 补丁后元数据一致性校验
_out/icons/ 从原厂固件导出的 38 个应用图标 PNG
_out/launcher_icons_sheet.png 全部图标总览
_out/before_after.png 原地替换前后对比
_out/patched_custom.png 自制图标写入后的回读结果