TIM-1EARCHIVE TERMINAL // 2026

NTE-008经验笔记CLASS · PUBLIC

初雪、龙娘与一台不肯下班的 Redmi 9A

从 DA 握手、EMI 初始化和 12 字节报错,到 4.5 GiB 分区里的 Android 16,再到一场自己制造的加密翻车:我和 Codex 把 Redmi 9A 折腾到了凌晨三点多。

DATE
2026.10.17
READ
12 MIN
LENGTH
4,576 字
CONTENTS · 目录(9)
  1. 任务很小,入口很多
  2. “缺了 12 字节”,缺的是谁的 12 字节
  3. 图形界面也给自己加了一道门
  4. 能读分区了,先把退路读出来
  5. 4.5 GiB 的箱子,装不下 9.776 GiB 的愿望
  6. 换系统、换内核、Root,要一层一层过
  7. 想省一次授权,反而多修了一次开机
  8. 最后一只小怪:界面说,BL 还锁着?
  9. 收工时,我们究竟完成了什么

凌晨三点多,这台 Redmi 9A 终于给出了两句让人愿意收工的话:

plain
unlocked: yes
uid=0(root)

在这之前,我们已经经历过黑屏、USB 异常、内存初始化失败、图形界面握手打转、ROM 容量对不上,以及一次好心办坏事的首次开机翻车。最后连“已经解锁了吗”都要再回 Fastboot 查一遍。

如果给这晚配一张立绘,我负责以初雪的形象抱着手机、按音量键、盯着屏幕;Codex 则是一只白紫色 GPT 龙娘,趴在日志和源码之间翻找线索。插画里的表情和小剧场是事后演绎,下面的数字与结论来自实际记录。

初雪和白紫龙娘在深夜工作台前研究手机与 USB 连接,桌上堆着日志、线缆和咖啡杯
FIG.01开工时以为只是刷个机。后来,连音量键都成了需要协调的现场操作。

任务很小,入口很多

最初的想法很朴素:把一台旧手机整理成适合远程控制和 App 操作的终端。Agent 的主要计算放在电脑或服务器上,手机负责常驻控制服务、接收指令和执行操作。Google 服务、应用兼容性和日常稳定性,比在手机上硬跑大模型更重要。

设备是国行 Redmi 9A,代号 dandelion,原系统为 MIUI V12.5.21.0.RCDCNXM。最早规划的是解锁 Bootloader、刷国际版、再获取 Root。官方解锁工具给过 168 小时等待提示,我们随后继续研究了 MTK 路线。

真正开始后,才发现“电脑连上手机”至少有好几种完全不同的含义:

plain
BROM → DA1 → EMI / DRAM 初始化 → DA2 → 读取分区表
                                         ↓
                              后续的备份、解锁与刷写

BROM 是芯片的早期启动入口;DA 是下载代理,负责后续与存储交互;EMI 提供内存初始化需要的参数。Android 里的 ADB、Bootloader 里的 Fastboot,又属于另外的运行状态。

所以,屏幕显示 Fastboot,不代表 MTKclient 的图形界面已经等到了它需要的设备。DA1 上传成功,也只说明前半段走通了。我们有一轮重启直接回到了正常 Android:那轮连 BROM 都没进入,自然不能拿来证明某个 DA 或 Preloader 不兼容。

这晚最费力的工作之一,就是反复确认:眼前这次失败,到底停在哪一层。

“缺了 12 字节”,缺的是谁的 12 字节

早期真正卡住我们的报错长这样:

plain
DA sync ...
Sending EMI ...
unpack requires a buffer of 12 bytes

看起来很像某个文件长度不对。顺着源码和传输日志查下去,含义却更具体:客户端准备把一个 12 字节的回复头拆成三个整数,但读取结果不够长。MTKclient 的状态读取代码能看到这段逻辑。

那次日志中,初始化请求先获得成功状态;主机随后宣布 EMI 长度,并记录了完整的 1712 字节发送数据。把这些数据重新拼起来算哈希,结果与提取出的 EMI 一致。之后没有收到预期回复,连续超时,最后才抛出这个异常。

因此,“12 字节”指向的是没有读完整的回复头,不能直接解释成 Preloader 文件少了 12 字节。它也没有替我们证明唯一根因:DA 与 EMI 的组合、手机端运行状态、传输异常,都还需要继续排查。

更有意思的是文件比较。当前国行固件、候选国际版和先前的一份提取结果,虽然完整 Preloader 文件不同,提取出的 EMI 却相同。后来找到的 “Redmi 9A Preloader Fix” 才出现了实质差别:

比较对象EMI 版本EMI 长度比较结果
前几份固件中的参数351712 字节内容哈希相同
Preloader Fix 中的参数351872 字节内容不同

版本号相同,里面的参数并不一定相同。反过来,文件名里写着 Xiaomi,或者 DA 里确实存在对应芯片记录,也不能证明整个初始化链路已经兼容。我们检查的几份 DA 中,有的根本没有相应记录;几份有记录的文件,其 DA1、DA2 内容也各不相同。

这里还有一个容易让人紧张的名字:--preloader。本次只读连接测试使用它提取内存参数,没有把这个 Fix 文件刷进手机的 Preloader 分区。该文件的完整原始固件版本也没有被可靠确认,我们没有把它当成可以随意刷入的底层固件。

初雪大概已经想把尾巴垫到手底下了。龙娘还在比较二进制文件。

图形界面也给自己加了一道门

我们当然想过直接用 GUI。毕竟有进度条、有按钮,看起来比较像“现在可以正常操作的东西”。

实际遇到了两类不同的问题。早先版本的界面在后端初始化失败后,连显示错误信息的控件都出过异常;较新的界面又出现过设备已经被识别、后续握手却反复失败的情况。

查调用链时发现,新版 GUI 完成前一段初始化后,还会进入共享连接函数。那条路径会关闭已经建立的 USB 连接,再重新打开并握手。手机已经进入后续协议状态,电脑却又从开头打招呼,两边就可能对不上了。

这处代码加了一个很小的检查:连接仍有效、目标配置也已经取得时,复用当前连接。五种离线路径自检通过。不过,最终成功的真机流程使用了命令行;修过的 GUI 没有再做一轮真机复测,所以不能把最终成功归功于这个补丁。

真正走通的组合是 MTKclient 2.1.4、SP Flash Tool v5.2020 包里的 MTK_AllInOne_DA.bin,以及那份 1872 字节 EMI 参数。2026 年 10 月 17 日 00:11:47,命令行测试开始;约 6.2 秒后,DRAM 初始化通过、DA2 启动,43 个分区出现在输出里。

折腾了那么久,成功时居然只要几秒。

我们也讨论过是不是音量键松得太早。成功那轮确实协调了按键时机,但 DA、EMI 和执行路径也发生了变化。没有做单变量对照,就不能宣布“原来全是没按住”。这次值得留下的是已经验证的完整组合和明确的阶段反馈,而不是一个听起来很顺口的唯一原因。

能读分区了,先把退路读出来

读到分区表时,我们还没有解锁,更没有刷进新系统。

接下来先保存了 20 份原机关键分区备份,包括启动相关分区、解锁配置,以及设备自身的重要配置分区。解锁写入后做了读回核对,再进入 Fastboot,实际取得 unlocked: yes。备份、写入返回成功、重启后确认状态,是三个分别检查的环节。

用户数据清除也经过了明确确认。原机分区备份不能代替照片、聊天记录和验证器等个人数据的可用备份;本次是在允许清空数据的前提下继续的。

随后装入官方 TWRP,核对 Recovery 分区读回哈希,进入界面检查触控、存储和重启。准备刷 crDroid 前,又补存了完整 super、boot、Recovery、dtbo 和三份 vbmeta,共七份镜像;电脑与手机端的 SHA-256 全部一致。

这一段没有什么戏剧性的按钮,主要是等待读取和比对。可它让后面的选择有了底气:知道自己改了哪里,也保留了改动前的内容。

4.5 GiB 的箱子,装不下 9.776 GiB 的愿望

最初考虑国际版 MIUI,是因为希望 Google 服务省心、变化少。但实际解包后,一个关键事实改变了路线:我们检查的 Global V12.5.7.0.RCDMIXM 使用 32 位 Android 用户空间,ABI 是 armeabi-v7a,armeabi,64 位 ABI 列表为空,Zygote 配置为 zygote32。

硬件支持 64 位,不代表当前系统就能安装只有 arm64 原生库的 APK。单独换一个内核,也不会把整套 32 位用户空间变成 64 位。考虑到后续 App 与控制工具的兼容性,我们开始比较 64 位 ROM。

于是迎来了本晚最适合画成漫画的一关。

双格漫画:龙娘试图把标着 9.776 GiB 的巨大箱子塞进 4.5 GiB 小箱子,初雪伸手制止;换成 4.163 GiB 箱子后两人松了口气
FIG.02这里比较的是安装脚本要求的逻辑分区大小,不是 ZIP 下载体积。箱子的比例是漫画演绎。

我们拿到的 AxionAOSP_2.8-Oneira-EROFS-blossom-20260922.zip,安装脚本要求的五个动态分区合计 9.776 GiB。这台手机现有 GPT 中,super 只有 4.5 GiB。脚本还会先移除原有逻辑分区组。

因此,这个具体 ZIP 在当前布局下不能直接安装。不是“先刷一下看看”的问题。我们没有为了迁就它去扩大分区,也没有据此给所有 Axion 版本下结论。

接着看 crDroid 的 blossom 设备页,候选变成了 crDroidAndroid-16.0-20260923-blossom-v12.12.zip。这一份又出现一个看起来吓人的数字:动态分区组的上限约 6.5 GiB,还是比 4.5 GiB 大。

如果只盯着这个字段,它似乎也应该被淘汰。但实际五个逻辑分区的分配量合计为 4,469,792,768 字节,约 4.163 GiB。检查 AOSP liblp 的分区构建逻辑后,可以区分出两个概念:组上限限制该组可用到多少,实际增长分区时还要另行检查物理空间;上限数字本身不等于已经占用的空间。

我们按实际分区大小计入对齐与元数据余量,确认有容纳空间,再通过真机安装验证。全程没有修改 GPT,也没有扩大 super。

这里甚至有三本账:外层 super 的容量、逻辑分区分到的空间、分区内部文件系统还剩多少。后来安装 Google Core 后,product 文件系统仍有约 686 MiB 可用;它和外层尚未分配的空间,是不同的数字。

这也是为什么下载页的 ZIP 大小,不能直接回答“我的手机装不装得下”。

换系统、换内核、Root,要一层一层过

我还选中了一份 4.19.325 自定义内核的 v2.2 boot 镜像。匹配它时,我们没有只看“同样写着 Redmi 9A”。

把候选 crDroid 的 boot 与这份镜像拆开比较后,ramdisk、DTB、启动参数、header version 和 page size 都一致。这个结果比同机型名称更有说服力,但仍只能支持继续测试,不能代替相机、传感器或长期运行验证。设备维护者的安装说明也提醒了自定义内核可能带来的问题。

实际执行顺序因此分成三层:

  1. 先装 ROM 和推荐的 Google Core,用 ROM 自带内核开机。 crDroid 整包校验通过;NikGapps Core 的压缩包与内部包检查通过,安装后关键 Google APK 逐一比对哈希。
  2. 系统基线建立后,再换指定内核。 运行时版本从 4.19.275-Jujutsu-Kernel@V6 变为 4.19.325-dandelion,再检查系统启动和基础状态。
  3. 最后处理 Root。 使用官方 Magisk 30.7 随包工具,在这台手机上修补最终选定的 boot,再刷入并验证。

原因很实际:ROM 包会写 boot;我们选的内核文件也是完整 boot 镜像。顺序颠倒,前一步刚做的修改就可能被后一步覆盖。

Magisk 修补会改变启动镜像,因此“换内核时只改变 kernel 段”的比较结论,不能原封不动套到 Root 之后。我们另外核对了修补结果:内核、DTB 和启动参数逐字节保持一致,Magisk 的初始化组件与载荷对应官方 APK,原始 init 也保留在镜像备份中。本次保留了 verity 与强制加密相关配置。

Magisk 官方安装文档是核对镜像与修补方式的入口。具体到这次操作,安装 APK、写入 boot、应用初始化、获得授权后的 Root 命令,仍然是不同的步骤。

不过,在顺利走完这些之前,龙娘还亲手给我们加了一关。

想省一次授权,反而多修了一次开机

ROM 和 Google Core 已经装好,准备第一次进入新系统时,Codex 做了一件看似贴心的事:提前把这台电脑的 ADB 公钥和 USB 调试配置写进新数据分区,希望首次开机后能直接继续远程操作。

结果,手机回到了 Recovery。日志里出现:

plain
set_policy_failed:/data/misc

这次问题出在我们增加的操作上。

新系统第一次启动要建立文件级加密,也就是 FBE 的目录策略。提前创建并放入文件的 /data/misc 等目录,妨碍了这一阶段的初始化。回头检查实际 ROM 的 init 配置,并对照 Android 16 的 init.rc,可以看到相关目录要求设置加密策略。

修正方式是重新格式化这次尚未投入使用的数据分区,清掉预置文件,让 Android 自己完成首次初始化。ROM 与 Google Core 所在分区保留,之后由我正常完成开机设置、开启调试并授权电脑。

这里的经验很具体:刚格式化后的目录,不能因为“看起来还是空的、我也有权限”,就假定已经适合写入后续运行配置。 文件系统生命周期和加密策略还没有走完。

小剧场里的龙娘大概会把“我先帮你省一步”那张便签,默默翻过来,写上“等系统初始化完再说”。

重新开始后,首次启动仍需要时间。我在手机旁等它进桌面,Codex 等调试连接恢复,再继续内核与 Root。那段等待没有被算成新的刷写失败,也没有靠反复重启来催它。

最后一只小怪:界面说,BL 还锁着?

系统起来了,Magisk 也完成初始化了。我看着 MemeKit,却发现它仍然显示 Bootloader 锁定。

如果前面的几小时真有背景音乐,这里应该突然停一下。

检查后,Android 属性确实报告:

plain
ro.boot.vbmeta.device_state=locked
ro.boot.flash.locked=1
ro.boot.verifiedbootstate=green

但进入 Fastboot,直接查询解锁状态,返回的仍然是:

plain
unlocked: yes

我们还查看了本地 MemeKit 源码:它优先依据 ro.boot.vbmeta.device_state 判断,因此把这一组属性显示成了 locked。对于这台已经刷入自定义系统的手机,这个界面值与 Bootloader 的直接查询结果发生了冲突。

这次没有因为界面的 locked 去尝试再次解锁,更没有回锁。最终判断使用实际 Fastboot 返回,并保留了两边不一致的记录。

Root 同样没有停在“应用显示已安装”。最后一次从 Fastboot 重启回系统后,授权 Shell,执行 su -c id,实际取得了 uid=0(root),上下文为 u:r:magisk:s0。完整 boot 分区读回哈希与电脑上的修补镜像相同,Recovery 仍与已验证的 TWRP 一致。

验证结束后,临时 Shell Root 授权被撤销,操作期间的 USB 充电常亮设置恢复为原值,手机端临时修补目录也清理了。以后真正需要 Root 的应用,仍按正常流程单独授权。

收工时,我们究竟完成了什么

截至 2026 年 10 月 17 日凌晨,这一轮完成的是手机基础环境的改造与验证:

项目实际结果
BootloaderFastboot 直接返回 unlocked: yes
RecoveryTWRP 3.7.1,进入、触控、重启与镜像读回检查完成
系统crDroid 12.12,Android 16 / API 36,arm64-v8a
Google 基础组件NikGapps Core 已安装,关键 APK 哈希和五个 Google 包注册状态已核对
内核运行时为 4.19.325-dandelion
Root官方 Magisk 30.7,最终重启后实际取得 uid=0(root)
加密与 SELinuxFBE 加密正常,SELinux 为 Enforcing
恢复材料原机关键分区备份、刷 ROM 前完整 super 等备份、原始与修补 boot 均保留

原计划中的国际版 MIUI 已被选定的 64 位 crDroid 路线替代,并非漏刷了一步。手机端具体的 Agent 控制服务还没有部署,那是基础环境验收后的下一项工作。

Google 登录与同步留给我实际验证;Play 认证、联网使用、相机拍摄、蓝牙、蜂窝网络和长期待机也还没有完成整套验收。相机服务枚举出两路相机、系统识别到传感器,只能说明相应服务与设备被看见,不能等同于拍照和测量都正常。

这台手机继续保持 Bootloader 解锁。以后更新 ROM,可能覆盖当前内核或 Magisk,需要重新检查;这次的开机与 Root 成功,也还不能替它签发一张“从此稳定”的证书。

疲惫但开心的初雪与龙娘轻轻击掌,手机已经进入桌面,电脑显示 unlocked: yes 和 uid=0(root)
FIG.03通关画面只放两行真正拿到的反馈。Google 登录和日常使用验收,留给下一次正常拿起手机。

回头看,最值得记住的不是哪一条命令特别神。是遇到一个“成功”时,继续问它具体成功了什么;遇到一个“失败”时,先看它究竟停在哪一步。

DA 上传成功,没有替我们证明 DRAM 可用。ZIP 校验通过,没有替我们证明分区放得下。写入完成,没有替我们证明系统能启动。桌面出现,也没有替我们证明 Root 已经生效。

但当这些检查终于一项项接起来,最开始那句“现在可以松手了吗”,终于有了一个很踏实的答案。

可以了。这次先让初雪和龙娘都歇一会儿。


插画由 AI 根据我提供的形象参考重新绘制。初雪形象来自《明日方舟》;白紫龙娘作为本文中 Codex 的非官方拟人形象,并参考了社区讨论的 ZipZipPipe GPT 龙娘方向,相关署名线索见社区来源记录。原始角色参考图和游戏界面没有随文转载;插画用于演绎这次协作的心情。