真机 meow_ui_time_ms() 返回「低 32=秒×1000、高 32 随机」的根因不是 SDK 写法,
而是 zig 0.17 自带 musl 在 mipsel 上的 time64 ABI 自相矛盾:
- zig 头(mips-linux-musl/bits/alltypes.h)无条件 64 位 time_t、struct timespec 16 字节;
- zig 的 clang 给 32 位 mips 预定义 __ILP32__,musl 的 time64 回退分支
`#if defined(SYS_x_time64) && !defined(__ILP32__)`(本意只排除 x32)被编译掉;
- 于是 libc 的 __clock_gettime64 实为 o32 time32 实现(SYS_clock_gettime 4263),
只写 8 字节 {int32 sec; int32 nsec},与头的 16 字节布局错配。
修复(patch_v2/sdk/meow_ui_time.c):
- meow_ui_time_ms() 直接按内核 o32 ABI 取 8 字节 time32,再升 64 位毫秒;
- 同 TU 强定义 __clock_gettime/__clock_gettime64(归档先于 -lc,链接期接管,
time()/gettimeofday()/pthread 定时等待的内部取时随之修正);
- 同源缺陷一并修:__nanosleep_time64/__clock_nanosleep(_time64) 转换 time32 后重发
(修复前 usleep(16000) 等亚秒睡眠立即返回、变忙等空转);
- gfx/sys/input/cachegov 四处重复 now_ms 收敛到 meow_ui_time_ms;
- build_sdk.sh OBJS 增加 meow_ui_time;新增 test_meow_time 宿主单测、
tools/clock_abi_probe.c 探针、CLOCK_ABI.md 根因/验证/影响面文档;
- 26 个宿主测试的编译注释补 meow_ui_time.c(cachegov 测试补时钟源)。
验证:宿主单测 PASS;anim(32)/view_event 宿主用例全过;build_sdk_host.sh --verify
76 成员全 MIPS32r2、0 warning;探针链接后无 libc cgt_init/__vdsosym,
__time64/__timedwait_cp 的取时指向本 TU,睡眠走 4166/4265 且用本地 8 字节结构。
9.3 KiB
SDK 时钟 ABI 说明(meow_ui_time_ms 真机故障的根因与修复)
面向对象:维护 SDK / 工具链 / 重链 App 的人。结论先行:这不是 SDK 代码写错,
是 zig 0.17 自带 musl 在 mipsel 上的 time64 ABI 自相矛盾;修复落在 SDK 链上
(patch_v2/sdk/meow_ui_time.c + 链接期强定义),改后需重编 meow_ui.a 并重链四 App。
1. 现象(真机)
M10 抓到的日志首列(meow_ui_time_ms(),64 位)可拆成:
6607363222717682072 → lo=1135000 hi=1538396632
8754846870717682072 → lo=1135000 hi=2038396632
18138485921460348192 → lo=1140000 hi=4223195352
- 低 32 位 = 开机秒数 × 1000,恒为 1000 的倍数 → 毫秒位丢失;
- 高 32 位每次调用随机、无单调性。
同进程另一份取时(旧容器编的 core)正常 → 问题只在「zig 0.17 编的那部分」。
2. 根因链(三段证据)
2.1 zig 头文件:time_t 是 64 位,struct timespec 16 字节
<zig lib>/libc/include/mips-linux-musl/bits/alltypes.h:
#define _REDIR_TIME64 1
#define __USE_TIME_BITS64 1
typedef _Int64 time_t; /* 无条件 64 位 */
struct timespec { time_t tv_sec; ...; long tv_nsec; ... }; /* tv_sec@0(8B)、tv_nsec@8 */
include/generic-musl/time.h 因此在 _REDIR_TIME64 下把 clock_gettime 重定向到
__clock_gettime64。用该链编译的 C 代码一律按「16 字节、tv_sec 8 字节」取数
(patch_v2/sdk/meow_ui.a(meow_ui_gfx.o) 的旧 meow_ui_time_ms 反汇编:lw 0x10/0x14
取 tv_sec、lw 0x18 取 tv_nsec,正是这个布局)。
2.2 zig 的 clang 给 mipsel 预定义了 __ILP32__
$ echo | zig cc --target=mipsel-linux-musleabi -dM -E - | grep __ILP32__
#define __ILP32__ 1
(arm-linux-musleabi 同样为 1;x86-linux-musl / aarch64 为 0。容器里的 gcc 对 mips o32
不定义 __ILP32__。)
2.3 musl 的 time64 回退分支被这句 !__ILP32__ 整体编译掉
<zig lib>/libc/musl/src/time/clock_gettime.c:
#if defined(SYS_clock_gettime64) && !defined(__ILP32__)
... __syscall(SYS_clock_gettime64, clk, ts); /* 4403 */
... 失败(ENOSYS) 再 __syscall(SYS_clock_gettime, clk, ts32); /* 4263 + 宽度转换 */
#else
r = __syscall(SYS_clock_gettime, clk, ts); /* 直接把 8 字节 time32 写进调用者缓冲 */
#endif
这句 !__ILP32__ 本意只排除 x32(__ILP32__ 是 x32 的标记),但 clang 把它当成
「32 位数据模型」的通名。于是 zig 的 mipsel libc 走进 #else:
__clock_gettime64 实际执行 SYS_clock_gettime(4263),只写 8 字节
{ int32 tv_sec; int32 tv_nsec; }。
证据(反汇编,当前 zig 0.17 静态链接的探针):
0002576c <__clock_gettime64>:
257c8: addiu $2,$zero,0x10a7 ; 4263 = SYS_clock_gettime(time32)
257cc: syscall ; $5 直接是调用者的 16 字节 struct 指针,无 4403 尝试、无转换
同一符号在旧容器产物 patch_v2/ncm_app/ncm(已入库,gcc 构建)里是:
115748: addiu $2,$zero,0x1133 ; 4403 = SYS_clock_gettime64
11574c: syscall
...
115774: addiu $2,$zero,0x10a7 ; ENOSYS 后回退 4263,写入本地 ts32
...
; 再把 ts32 升成 64 位写回调用者(tv_sec=tv_sec、tv_nsec=tv_nsec)
两者对比即根因:头文件 ABI(64 位 time_t)与 libc 实现 ABI(32 位 time32)不一致。
2.4 数值解释(为什么「低 32 = 秒×1000、高 32 随机」)
libc 把 {sec, nsec} 当两个 int32 写进 16 字节缓冲的前 8 字节;SDK 按 64 位读:
tv_sec = sec | (nsec << 32) // 低字正确、高字=纳秒
tv_nsec = 缓冲偏移 8 的未初始化值 // 毫秒项≈0
result = tv_sec*1000 + tv_nsec/1e6
= sec*1000 + (nsec*1000 mod 2^32)<<32 + 垃圾/1e6
- 低 32 位只剩
sec*1000(nsec 项左移 32 位不落到低字)→ 恒为 1000 的倍数; - 高 32 位 =
nsec*1000 mod 2^32→ 每次随机、不单调; tv_nsec来自越界读 → 通常是小值/0(真机上恰好 0,所以没有额外尾数)。
3. 修复(patch_v2/sdk/meow_ui_time.c)
- 取数走内核 o32 ABI:
meow_ui_time_ms()用syscall(SYS_clock_gettime32, CLOCK_MONOTONIC, &{int32 sec; int32 nsec;}), 再meow_time_ms_from_parts(sec, nsec)升成 64 位毫秒。完全不依赖 libc 头声明的struct timespec宽度 —— 即使将来 zig 修好(或换回容器链),内核 time32 入口始终存在。 - 链接期修 libc 入口:同一 TU 强定义
__clock_gettime/__clock_gettime64(musl 的time()、gettimeofday()、__timedwait_cp()(pthread 定时等待)内部都调__clock_gettime)。应用链接顺序是... meow_ui.a ... -lc,归档先解析符号,libc 里那份 坏实现(及其 VDSO 前导)不会被拉入 —— 用nm验证链接产物里没有cgt_init/__vdsosym, 且__time64/__timedwait_cp的jal指向本 TU 的实现。 - 睡眠入口一并修正(同源缺陷):
__nanosleep_time64/__clock_nanosleep(_time64)先按 time32 转换再发SYS_nanosleep/SYS_clock_nanosleep,rem再扩回 64 位。 修复前 zig libc 把 16 字节 time64 结构直接交给 time32 系统调用,内核读到的 nsec 恒为 0 → SDK 里usleep(16000)(帧节拍、输入轮询、IME 冷却)全部变成忙等空转。 - 去掉四处重复的
now_ms():meow_ui_gfx.c/meow_ui_sys.c/meow_ui_input.c/meow_cachegov.c统一调meow_ui_time_ms()(cachegov 保留宿主测试注入桩)。 - 构建脚本
build_sdk.sh的 OBJS 增加meow_ui_time;宿主单测test_meow_time.c固定换算边界与「单调且前进」;探针tools/clock_abi_probe.c用于宿主/真机一眼验收。
非 mips 目标(宿主单测、aarch64 宿主)走普通 clock_gettime,行为不变。
4. 验证
宿主:
cd patch_v2/sdk
gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o /tmp/meow_ui_time.o
gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c /tmp/meow_ui_time.o -o /tmp/test_meow_time
/tmp/test_meow_time # == PASS ==(换算边界 + 单调前进)
交叉:
bash patch_v2/sdk/build_sdk_host.sh --verify # 成员清单/架构自检 + 0 warning
llvm-objdump -d meow_ui.a(meow_ui_time.o) # meow_ui_time_ms: syscall 4263, lw 0x18 取 nsec、sra 扩展 tv_sec
链接期接管(用真实归档链一个探针,检查符号来源):
# 不应出现 libc 的 VDSO 前导(cgt_init/__vdsosym)→ libc 的取时实现未被拉入
strings -a clock_abi_probe_sdk | grep -c __vdso # 期望 0
llvm-nm clock_abi_probe_sdk | grep -E 'clock_gettime|nanosleep'
# __time64 / __timedwait_cp 的 jal 应指向本 TU 的 __clock_gettime;睡眠走 SYS_nanosleep(4166)/clock_nanosleep(4265)
探针 tools/clock_abi_probe.c(宿主 gcc 或 mipsel 交叉;加 -DMEOW_PROBE_WITH_SDK
并链 meow_ui.a 可同时验收 meow_ui_time_ms):把 struct timespec 预填 0xAA 后取时,
若偏移 8..11 仍是 0xAA 即说明 libc 只写了 8 字节(ABI 不一致),退出码 2。
真机(塔台批准后):日志首列应连续、出现非整千差;meow_ui_time_ms() 两次取样差 = 真实毫秒。
本次设备已离线,真机确认待办。
5. 影响面
- SDK 内:
meow_ui_time_ms()及其上层 —— 动画 tween(meow_ui_anim.c)、滚动/fling (meow_ui_scroll.c/meow_select.c)、IME 冷却(meow_ui_ime.c)、状态栏动画 (meow_ui_gfx.c)、通知 TTL(meow_ui_notify.c)、输入层 tick/启动冷却 (meow_ui_input.c)、cachegov 回退窗口(meow_cachegov.c)、日志首列(meow_ui_log.c)。 - App 侧:
on_tick入参now、meow_anim_*、以及任何自行调用meow_ui_time_ms()的 App(hello/reader/files/ncm…)。 - 产物:
meow_ui.a变更 → 需重编并重链四 App(ncm/reader/files/hello);time()/gettimeofday()的修正也在同一份归档里生效(链接期覆盖)。 - 真机写入红线未触碰;本单只改 SDK 源与构建脚本。
6. 同源残余(未在本单修复,需单独安排)
同一句 !__ILP32__ 还打穿了以下路径(睡眠族已在上文一并修掉,余下这些症状不同、
修复面更大):
| 路径 | 症状 | 说明 |
|---|---|---|
futex 相对超时 |
亚秒 timed wait 立即超时 | __timedwait_cp 的 __futex4_cp 同款 !__ILP32__ 守卫;pthread_cond_timedwait 的「now」已由本单修正,但相对超时仍按 8 字节读(>1s 的整秒等待基本可用,亚秒等待失效) |
timerfd_settime |
到期时间错乱 | struct itimerspec(32 字节)当 16 字节读;当前 SDK 未用 timerfd |
clock_settime/clock_adjtime/pselect/ppoll/sigtimedwait/semtimedop 等 |
未验证 | 同族守卫;SDK 目前未直接使用 |
建议:向 zig 上游报 musl __ILP32__ 守卫问题(clang 对 32 位目标统一定义 __ILP32__,
而该守卫本意只排除 x32);根治应换用容器旧链(gcc,不定义 __ILP32__)编 SDK,或
downgrade/修补 zig 内置 musl。不要指望 -D_TIME_BITS=32:zig 的
alltypes.h 是无条件 #define __USE_TIME_BITS64 1,命令行改不动;而只改用户侧头文件会让
用户结构(8 字节)与 libc 内部(16 字节,pthread 等)再次错配。