我的老手机启动不起来了
刷机全是 OKAY,手机却连开机动画都进不去。从 Fastboot 找日志,一路查显示、ECC、CPU 和联发科电压初始化,最后靠四个小核重新进入 MIUI。
手机是红米 K30 至尊纪念版,代号 cezanne,天玑 1000+,四个 A77 大核加四个 A55 小核。Bootloader 已经解锁。前阵子我妈跟我说这手机充电冲着冲着就无限重启然后卡米了,当时也没太放在心上,以为是自动系统更新,然后更新到一半出故障了之类(这台 MIUI 版本还想当老,没记错的话是没有 A/B 分区的)。
本来以为这台手机重新刷个系统就好了,结果一路折腾到了 ARM 的错误状态寄存器、设备树,还有联发科的 CPU hotplug。系统是救回来了,代价是四个大核没了。这下确实是四核有难,四核围观了。准确说,四个大核被留在了 offline,最后只靠四个 Cortex-A55 小核进了 MIUI。没换主板,没换电池,也没拿风枪吹 CPU。至于 CPU 本体到底坏没坏,估计是坏了,但也不好说具体是出什么问题了。
这次排障是我和 Codex 一起做的。我负责按按键、看屏幕、切模式,它在 Mac 上读日志、翻源码、写工具、改镜像。中间好几次以为”找到了”,转头又无限重启了。只能说 Astra 大人神力啊。
之前我在另一台 Windows 电脑上用 MiFlash 刷过官方线刷包,选的”全部删除”,版本是基于 Android 12 的 MIUI 13。刷机全程没报错,每个分区都是 OKAY,但手机还是在 REDMI 标志附近重启,连 MIUI 开机动画都没见着。Fastboot 倒是稳得很,能刷,并且也没见得报错。
GPT 这傻逼模型总有坏习惯,总是不把用户说的当一回事情,我第一次问它时候,它倒是先不质疑手机出了什么问题,而是用户出了问题 😅。又是让我判断“是否真的卡米”又是让我判断“刷机包我没下错吧”。实在是气死我了😡。于是新开了一个 session 一开始我就强调了:包没选错,刷机也确实成功了,别再让我检查型号。手边没有好电池,没有维修电源,手机原来是怎么坏的我也不知道。能用的就是这台 Mac、一根 USB 线,和一个还活着的 Fastboot。剩下的事情它自己解决。
然后你们还可能会关心:我用了多少 token?我这次只在同一个 session 里就把事情搞定了,按照当前的 API 价格来算的话,花了 $58.19,换算成我的 $20 账号的周额度 60%。如果按照这样换算的话,那相当于我只花了 $3 等效的价格就解决了整件事(至少 partially)还是相当划算的 😁。
Fastboot 好好的,为什么系统起不来?
CPU 要是真坏了,怎么还能在 Fastboot 里我刷机的时候没出问题?但后来仔细一想,recall 我之前写过的 Learning Linux Kernel (Part 2) - Bootstrapping 把启动过程拆开看就懂了。Fastboot 跑在 bootloader 里,执行的代码、启用的硬件、电源管理状态都非常有限。进了内核才有更多的 CPU 初始化、调频、驱动加载,再往后才轮到 Android 用户空间。
当然,反过来也不能因为系统起不来就说刷机失败了。后来拿到 Recovery 的 root shell 之后,我们把 64 MiB 的 boot 分区整个读回来算 SHA-256,跟原厂镜像一模一样;128 MiB 的 Recovery 分区也跟当时刷的诊断镜像一致。所以,这至少排除了说 UFS 有问题。非常好,我们现在怀疑的点至少又少了一个。
日志从哪里拿?
平时 Android 出问题,第一反应是 adb logcat。这次没这待遇,Recovery 都进不去,没有 ADB shell 给我操作。
好在联发科的 bootloader 留了口子。顺着 LK 的字符串和源码找,能翻到 oem lkmsg、oem lpmsg、oem dump_pllk_log 这类命令,可以把存下来的内核 console、消息缓冲区、preloader/LK 日志读出来。
于是 Codex 大人花了一些 token 用 Python、PyUSB 和 libusb 写了个小工具,直接处理 USB 协议:发命令,读 INFO,遇到 DATA 按长度接收,最后等 OKAY。总算捞出了点 log:256 KiB 的 console buffer、64 KiB 的消息 buffer,还有 bootloader 日志。
随后就是漫长的重启 -> 崩溃 -> bootloader 看 log 环节。至于为什么反复重启了不下十次,是因为经常遇到读取出来的 log 和上一次一模一样 😅 LK 会从 expdb 里恢复以前的异常记录。console buffer 是固定大小循环写的,前半截是新内容,后半截可能还留着旧的。有一次 Recovery 明明跑得好好的,导出来的 OEM 日志却跟上次崩溃的字节完全一样。
所以 Codex 大人开始给每轮抓的日志编号,存原始数据、时间和哈希,对比哪些内容真变了。第一份像样的内核异常落在 LED/背光设备注册路径上,栈大概长这样(省略中间一些信息):
__pi_strcmp
class_find_device
of_led_classdev_register
devm_of_led_classdev_register
mtk_leds_probetext手机黑屏,栈里刚好有 LED,是不是很合理?差点就开始怀疑背光硬件了。但 strcmp 收到坏指针,只能说明程序在这撞墙了。是驱动自己传错了,还是前面什么地方把数据弄坏了,单看这段分不出来。为了看得更清楚,我们不动内核,只在启动参数里加:
ignore_loglevel loglevel=8 initcall_debugtext这次日志里 mtk_leds_init 正常返回了 0。“背光坏了”这个说法,已经解释不了看到的现象了。真正有意思的是后面的内容,摘几行关键的:
ecc error,irq_index:32, misc0_el1: 0x0000bd02a0018184, status_el1: 0x6e000007
ecc error,irq_index:36, misc0_el1: 0x0000e00180000e80, status_el1: 0x4e000006
ecc error,irq_index:33, misc0_el1: 0x0000ca0180001e40, status_el1: 0x4e000006
read_timeout_handler:33: read timeout
Insufficient stack space to handle exception!text这比”某个驱动空指针”具体多了。对应的 MediaTek cache_parity 驱动会读 ARM 的 ERXSTATUS_EL1、ERXMISC0_EL1 这些寄存器,0x6e000007 里有效位和不可纠正错误的标志都在,后面还跟着总线读超时。
日志已经指向 CPU/缓存这条路了,最直接的对照实验就是:启动时少开几个核。我们加的是:
maxcpus=1text这回在带详细日志的 Recovery 上重做,才拿到了能验证的结果。终于是让Recovery 进去了。 随后又是发现小米官方的 Recovery 没有 ADB。原厂 Recovery 下电脑看到 unauthorized,手机上又没弹授权框。我点了”连接小米助手”,设备变成 sideload,普通 adb shell 还是 closed。最后的办法是:在已经能启动的 Recovery 上改 ramdisk 里的 ADB 属性和 init 配置,内核、设备树、菜单全都不动,让它给个临时 root ADB。有了 shell,终于不用猜了,直接可以读 log。总而言之 经过漫长的调试流程,最终的 Recovery 可以在开四个小核的情况下跑起来,非常非常好👍。
online: 0-3
present: 0-3
possible: 0-7text能不能只关掉那个坏核?
这时候我的想法很朴素:四个大核全关太亏了,能不能找出到底是哪个有问题,只关它一个?Bootloader 都解锁了,不用白不用。 于是开始做”四个小核 + 每次放行一个大核”的 Recovery。
实际做法是:把其余大核节点的 enable-method 删掉,让它们过不了 CPU operations 初始化,再调 cpu-map,同时保持原节点顺序和硬件 ID 不变,免得测试的时候把核心编号搞混。
CPU 4 差点让我觉得没有问题:MIDR 0x411fd0d0 确认是 Cortex-A77,频率 2.6 GHz。然后它自己重启了,没人按。ADB 再连上,uptime 已经清零重数,随后连接又断了。随后经过了漫长的实验和多次的人工干预进入 bootloader,发现好像一旦开启任何一个大核,都会导致各种各样的问题。。。
| 配置 | 实际拿到的结果 |
|---|---|
| 四小核 + CPU 4 | 进入 Recovery,三次绑核校验正确,随后出现无人工干预的 uptime 重置;该配置期间抓到新的 ECC/超时记录 |
| 四小核 + CPU 5 | 第一轮自动回 Fastboot,读到的是旧日志;重试后有新的 CPU 5 工作记录和总线超时,未取得稳定 ADB |
| 四小核 + CPU 6 | 短暂进入 Recovery;随后抓到新的 ECC 记录,包括不可纠正错误标志 |
| 四小核 + CPU 7 | 抓到一份 alloc_vmap_area 的内核异常 |
这里还有我自己的锅:中途我一直说”黑屏重启”,后来想想,有几次我只看到了黑屏,并没有确认它真重新走了一遍启动流程——可能是卡住,也可能只是启动慢。
经过一番折腾
既然逐核找不出能留的大核组合,那就先全屏蔽,能用再说。很自然的想法:把四个大核的 enable-method 全删了,只留四个小核。结果这个版本照样黑屏,但日志给了一个跟之前不一样的原因。内核跑了三十多秒,开始反复打印:
[CPU][EEM]@eem_init01():3389, get_volt(EEM_DET_B) = 0x00000060, VBOOT = 0x00000038text联发科的 EEM 电压初始化在等大核簇电压达到预期值。CPU 从拓扑里删掉了,配套的电压初始化流程可没跟着删。这份日志里没有新的 ECC 记录,基本就是这一句在刷屏。说明这次至少卡在初始化等待上,之前那套 CPU 故障的解释盖不住所有现象。
行吧,驱动有它自己的想法。于是把之前确实能启动的那份 Recovery 恢复回去,设备树不动,继续用 maxcpus=1。又成功了。在线 CPU 一直是 0-3、缓存和总线错误状态一直是零。总算有个能往前走的基线了。
前面那么多轮动的都是 Recovery,Android 的 boot 分区一直是原厂内容,到这一步才第一次往里写修改后的 boot。 最终方案没改内核代码,没改 Android 的 ramdisk 和设备树,只是把成功 Recovery 的启动参数搬了过来:
bootopt=64S3,32N2,64N2 buildvariant=user ignore_loglevel loglevel=8 initcall_debug k30u_diag=singlecpu_boot maxcpus=1textk30u_diag 是我们自己塞的标记,方便认镜像;真正起作用的是 maxcpus=1。详细日志参数这次也一起留着。
刷 boot,重启。经过了大约 2分钟的等待,REDMI 过了。MIUI 动画出来了。就让它转着,不按按键,不换镜像。后来真的进了系统!!!一切都值得了。Astra 大人不愧是 AGI,以后修手机的人真的会失业了。。。
既然只剩四个 A55,能省一点是一点。我让 Codex 看了眼后台,把广告服务、使用统计、负一屏、快应用和它的辅助组件、传送门都禁了。用的 pm disable-user --user 0。本来还想让 Codex 把我动画调 0.5 倍,模糊效果也想关,结果 ADB 提示缺 WRITE_SECURE_SETTINGS。MIUI 还有个单独的”USB 调试(安全设置)“开关,开它居然要插 SIM 卡。我只是想改个动画啊,,,
另外,也顺便把系统更新禁用了。别到时候自动更新然后把修改的启动参数覆盖了。