Codex 帮我救了次砖:无限重启到 fastboot
前情提要
这次本来只是一次普通的刷机。
我手上的设备代号是 Redmi Note 12 Turbo,之前跑的是 crDroid 12 类原生系统,这不是 HyperOS 4 发布了嘛,我在用的移植包也更新了,那我肯定必须要尝尝咸淡,于是下完包,重启手机到 fastboot,运行线刷包里的脚本,过程看起来也没有什么特别的:刷入 boot、完整 firmware、recovery、vendor_boot、dtbo、vbmeta 和 super,最后设置 A 槽并重启。
不过问题就出在切换到 A 槽,我足足等了 5 分钟,后来实在是不想等了,于是就直接长按电源键重启了手机。
手机确实亮了 Logo,但立马又跳回了 fastboot。我还想确认 recovery 是否可用,于是执行 fastboot reboot recovery,但结果也是一样:重启、亮 Logo、立马回到 fastboot,像是手机在用一种非常坚定的方式告诉我——你完蛋啦!
一开始我不死心,以为这是典型的启动链不匹配:移植包的 boot、vendor_boot、dtbo、vbmeta 或 recovery 之间版本没对上。结果最后发现,真正有问题的东西,比这些镜像都更底层,也更容易被忽略:UFS LUN4 的 GPT 和槽位元数据异常了。
叠甲环节
本文是一次真实故障的复盘,不是可以无脑照抄的刷机教程。
gpt_both4.bin必须来自同机型、匹配当前存储布局的官方包,partition:4也不是所有设备都一样。GPT 写错可能直接让设备无法启动,严重时只能走 EDL 或售后维修,真的变成砖头了可千万不要来找我!同时,谨慎使用 AI Agent,务必审查所有的命令
事情是怎么开始
首先明确一下事情发生的原因:
正常运行 crDroid → 进入 fastboot → 执行 HyperOS 4 的 FlashWindows.bat → 重启 → 出现 Logo → 很快重新掉回 fastboot
于是我干脆打开 Codex,让 Codex 先只读检查一下当前的情况,结果如下:
只读检查完成,未执行重启、刷写、切槽或解锁操作。
目前判断:设备确实尝试从 A 槽启动,但连续失败后返回 fastboot。证据是:
- 设备:marble- 当前槽位:a- A 槽重试次数仅剩 5,通常初始为 7,说明至少发生过两次失败启动- A/B 槽均为 slot-successful: no- 两槽尚未被永久标记为 unbootable- recovery_a 和 recovery_b 分区都存在,大小均为 100 MiB- bootloader 已解锁- 电池电压约 4.16 V,供电正常- 没有未完成的虚拟 A/B 快照更新- 当前是 bootloader fastboot,而不是 fastbootd
最可能的原因是当前 A 槽的启动链不匹配或损坏,尤其是以下分区之间版本不一致:
- recovery_a- vendor_boot_a- dtbo_a- vbmeta_a- 相关固件分区
因此 fastboot reboot recovery 发出后,recovery 启动校验或早期初始化失败,bootloader 又回到了 fastboot。两槽 successful=no 本身也可能是刚刷机后尚未成功进入系统造成的,但 A 槽重试计数已经下降,能确认存在实际启动失败。
另一个较低概率原因是音量减键卡住,设备每次重启都被强制进入 fastboot;可以在不改写设备的情况下检查按键是否被壳体压住。
bootloader 没有提供 boot-reason,所以仅凭当前 fastboot 变量无法再定位到具体是哪一个镜像损坏。下一步若继续只读,可检查你本地准备使用的 ROM/recovery 文件,核对它们是否确实适配 marble 以及版本是否成套。其实这里有几个容易误判的地方,slot-successful: no 不等于槽位已经损坏。刚刷完系统、还没有成功进过一次 Android 时,它本来就可能是 no。但 A 槽的重试次数从默认值一路掉到 5,说明设备不是“根本没有尝试启动”,而是确实启动过,只是在早期阶段失败了,这只能说明设备确实尝试启动并失败,不能单凭这一点排除 super 未正确刷入。
Codex 还建议我先检查是不是音量下键被卡住了,确实是有人遇到过这种情况,不过我一没有戴壳,二确实是检查过了,音量下键没有卡住。此外,bootloader 还能读出 boot_a、boot_b、recovery_a、recovery_b 的容量,电池电压也正常,设备没有被回锁。于是问题范围暂时集中在启动链、分区读取和槽位元数据上,而不是电量、USB 或按键。
先做排除,而不是继续盲刷
事已至此,最简单、最容易做的决定就是再刷一次包,反正手机已经进不去系统了,感觉再坏也坏不到哪里去。
不过刷完之后还是给了我一个晴天霹雳,脚本依然在切换到 A 槽的阶段卡了好长时间,无奈强制重启,依然跳回 fastboot。
草!摆烂!放 Codex!
Codex 先尝试把官方 boot.img 临时送进内存启动。文件传输本身可以完成,但 bootloader 随后返回了 Failed to load/authenticate boot image: Device Error。后来另一次临时启动甚至在主机端等了超过一分钟,设备虽然还显示 fastboot,命令却不再响应。那一刻看起来又像是 USB 卡住了,Codex 没有继续发命令,而是先结束主机端进程;我按照 Codex 的建议拔掉 USB,长按电源键让手机彻底冷复位。
重新进入 fastboot 后,我把设备重新连接给 Codex。它确认 bootloader 仍然是解锁状态,电池电压也有 4.238V,没有低电量保护之类的问题。更有意思的是,A 槽的重试次数从 7 变成了 6:这说明手机刚才确实尝试过启动,只是失败后回到了 bootloader,而不是单纯被 USB 或某个按键带进了 fastboot。
冷复位之后,Codex 执行了最直接的启动测试:
fastboot continue结果还是原来的错误:
Resuming boot FAILED (remote: 'Failed to load image from partition: Device Error')到这里,USB 会话卡死和一次性的 UFS 状态异常基本可以排除了。设备能重新枚举,fastboot continue 也稳定地复现同一个错误,Codex 判断问题已经发生在 bootloader 尝试加载启动镜像的过程中。
接着让 Codex 试了另一个槽位
当时设备当前激活的是 A 槽,所以 Codex 还不能马上断定是“整个启动链坏了”。也许只是 boot_a 或 recovery_a 出了问题,而 B 槽还保留着一套能启动的内容。
于是 Codex 切到了 B 槽:
fastboot set_active bfastboot continueB 槽没有给出任何不同的结果,仍然是:
Failed to load image from partition: Device ErrorCodex 随后把当前槽位切回 A,再次确认状态。A、B 两个槽位都在 slot-unbootable: no 的状态,却都无法完成镜像加载。这个结果很重要:如果两个槽位都报完全相同的错误,继续来回切槽的价值就不大了,它们很可能共享某个更底层的依赖。
接下来 Codex 把怀疑对象放回了镜像本身。官方包里的 boot.img 已经核对过 CRC,Codex 把它直接写入 boot_a,写入阶段没有任何异常:
Sending 'boot_a' (...) OKAYWriting 'boot_a' OKAY但紧接着执行 fastboot continue,ABL 还是返回:
Resuming boot FAILED (remote: 'Failed to load image from partition: Device Error')这时 Codex 提醒我,fastboot 报告写入成功,并不代表启动阶段真的能从正确的位置读到它。文件可能已经被传给 bootloader,分区也可能接受了写入,但 ABL 仍然无法按自己的方式把它加载出来。
我让 Codex 把能想到的原因都排了一遍
接下来排查过程有点像逐个关掉报警器,我负责确认是否可以继续,Codex 负责执行对照测试。
Codex 先对 A 槽的 vbmeta 做了一次可逆的 AVB 对照,使用官方 vbmeta.img 临时关闭 verity 和 verification,再执行启动。结果不变,还是 Failed to load image from partition: Device Error。测试失败后,Codex 立刻把官方的 vbmeta 刷回去了,没有留下这个临时修改。
跨系统刷机还会让人想到 userdata 和 metadata。从类原生系统切换到 HyperOS,旧数据和加密元数据确实可能导致系统卡开机,所以在我明确确认清空数据之后,Codex 又清除了这两个分区,然后设置 A 槽重启。
手机还是回到了 fastboot。
Codex 解释说,这一步不能证明 userdata 或 metadata 永远不会造成启动问题,但至少说明它们不是这次“ABL 连启动镜像都加载不出来”的根因。因为即使数据已经清空,错误仍然发生在更早的阶段。
到这里,启动镜像本身、A/B 槽、AVB、旧数据、USB 状态,能想到的常见解释几乎都被排过了。更关键的是,所有失败都指向同一句话:
Failed to load image from partition: Device Error此时我已经吓尿了,就像是核弹爆炸,我整个人瘫坐在椅子上,这下得肛售后了。
这时候,Codex 开始怀疑分区表
Codex 把注意力从“镜像里面是什么”,移到了“bootloader 是怎么找到这个分区的”。
GPT 可以把它理解成 UFS 存储上的一张地图。boot_a 从哪里开始、占多大空间,recovery_b 在什么位置,bootloader 应该通过什么名字找到它,底层都要依赖这张地图。
如果镜像内容坏了,重刷一份正确的镜像通常就能改变结果;但如果地图本身出了问题,那么你把正确文件写进去,bootloader 仍然可能去错误的位置读取,或者按照异常的槽位信息处理它。于是就会出现一种很矛盾的现象:fastboot 能识别设备,能写入分区,所有命令都显示 OKAY,但 ABL 仍然说自己无法加载镜像。
这也解释了为什么前面的排查一直没有效果。A、B 槽虽然是两套启动内容,却仍然共享底层的存储布局;换槽只能换一套文件,不能自动换一张错误的地图。
不过 GPT 只是一个怀疑,不能凭感觉就去刷。下一步 Codex 回头检查了这次使用的线刷包和脚本。
GPT 文件在包里,但脚本没有真正使用它
Codex 打开官方包之后,确实看到了很多 GPT 文件:
gpt_both0.bin ~ gpt_both5.bingpt_main*.bingpt_backup*.binrawprogram*.xml我一开始也以为,既然这些文件都在包里,官方线刷肯定会顺手把分区表恢复一遍。可是 Codex 继续往下看脚本后,发现它真正执行的是刷入已经存在的分区:boot、recovery、super、各种 firmware 以及 userdata,最后再设置 A 槽并重启。
脚本末尾的关键部分是:
fastboot set_active afastboot reboot却找不到这样的命令:
fastboot flash partition:4 gpt_both4.bin这就解释通了。官方脚本默认设备原来的 GPT 是正常的,所以它只更新“地图上已经存在的房间”,不会每次线刷都重建整张地图。包里虽然准备了 GPT 文件,但它们只是躺在那里,并没有被这次脚本刷入。
也就是说,这次完整线刷把系统和启动镜像都更新了,却把已经异常的 LUN4 GPT 原样留了下来。无论再刷几次 boot,只要 ABL 仍然拿着那张错误的地图,结果都可能一样。
最后只改这一块
根据这台设备的分区布局,boot_a/b、recovery_a/b 等启动相关分区位于 UFS LUN4。现在问题已经指向了 GPT,但这仍然不是可以随便尝试的修复方式:LUN 编号不能猜,GPT 文件也不能从别的机型或别的存储布局拿来凑合。
所以在真正写入前,Codex 重新核对了设备代号、bootloader 解锁状态和官方 GPT 文件来源。我确认风险后,让 Codex 只写 partition,不碰其它 LUN,不碰持久化和校准分区。这个操作有可能把设备写到无法启动,必要时甚至要走 EDL,所以我是在了解风险后才批准继续的。
确认之后,Codex 实际执行的命令只有:
fastboot flash partition:4 gpt_both4.binfastboot reboot bootloaderSending 'partition:4' (44 KB) OKAYWriting 'partition:4' OKAY写完之后,Codex 没有马上执行一堆其它操作,而是先让设备重新回到 bootloader,再检查产品、boot_a 和 recovery_a 的分区容量、当前槽位、A/B 重试次数,以及 bootloader 的解锁状态。
Codex 把结果告诉我:启动分区容量正常,bootloader 仍然是解锁的,A 槽重试次数也回到了 7。这说明至少从分区表的角度看,设备已经重新按照官方布局识别这些分区了。
终于不再报错
最后只剩一个判定测试,Codex 执行了:
fastboot continue在 GPT 修复之前,这条命令每次都会立刻返回 Device Error。这一次,输出变成了:
Resuming boot OKAY我本来还在苦恼的,一抬头看见:
成功:重写 LUN4 GPT 后,fastboot continue 首次返回 OKAY,ABL 已能从分区装载启动镜像,根因已确认是 LUN4 GPT 元数据/槽位属性异常,而不是 ROM、boot、AVB 或卡键。现在监测设备进入官方系统、recovery 还是再次回到 fastboot。
当时我就怔住了,说有这么好的 AI 你怎么咋不早点告诉我?我直接从椅子上弹射起来了。
手机没有再像之前那样几秒钟后回到 fastboot,系统最终成功启动,看到熟悉的 Powered by Xiaomi HyperOS 之后,我都要哭了。
到这里,我和 Codex 基本可以坐实:问题不是某个系统镜像内容损坏,而是 UFS LUN4 的 GPT,以及与槽位解析相关的元数据异常。fastboot 之前之所以看起来“一切正常”,只是因为它还能枚举和写入;真正需要按照分区布局加载镜像的 ABL,才把问题暴露了出来。
至于异常究竟是在旧线刷流程卡在 set_active a 时产生的,还是在那之前就已经存在,仅凭 fastboot 的变量没法还原写入瞬间。但可以确定,真正让手机恢复启动的不是又刷了一遍 boot,而是 Codex 在我确认风险后把 LUN4 的地图重建成了官方版本。
写在最后
这次最刺激的地方,不是手机进了 fastboot,而是每个常见答案看起来都很合理:重刷 boot、重刷 recovery、切换槽位、关闭 AVB、清除数据,哪个单独拿出来都像是正确方向,但它们都没有改变那句 Failed to load image from partition: Device Error。
最后真正有用的,反而是那个平时几乎不会注意的 gpt_both4.bin。
我以前总觉得刷机出问题多少带点玄学,遇到无法解释的现象就再刷一次。现在看来,所谓玄学,很多时候只是还没有把“文件内容”和“设备如何找到这个文件”分开看。镜像可以是好的,写入也可以是成功的,但只要地图错了,启动程序还是找不到它。
这次能救回来,靠的也不是什么神秘操作:我把现场和授权交给 Codex,Codex 让设备自己把错误说清楚,再沿着错误发生的位置往下查,最后只改动已经确认的那一块。
其实……去年的这个时候,我也遇到了一样的问题,甚至也是从类原生刷回 HyperOS。只不过当时孩子小,还没开智,没用上 Codex,无奈扔去售后,售后也是直接黑了我 219 块钱,所以……为什么官方线刷包,脚本里没刷完整呢?