# 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 字节 `/libc/include/mips-linux-musl/bits/alltypes.h`: ```c #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__` 整体编译掉 `/libc/musl/src/time/clock_gettime.c`: ```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`) 1. **取数走内核 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 入口始终存在。 2. **链接期修 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 的实现。 3. **睡眠入口一并修正(同源缺陷)**:`__nanosleep_time64` / `__clock_nanosleep(_time64)` 先按 time32 转换再发 `SYS_nanosleep`/`SYS_clock_nanosleep`,`rem` 再扩回 64 位。 修复前 zig libc 把 16 字节 time64 结构直接交给 time32 系统调用,内核读到的 nsec 恒为 0 → SDK 里 `usleep(16000)`(帧节拍、输入轮询、IME 冷却)全部变成忙等空转。 4. **去掉四处重复的 `now_ms()`**:`meow_ui_gfx.c` / `meow_ui_sys.c` / `meow_ui_input.c` / `meow_cachegov.c` 统一调 `meow_ui_time_ms()`(cachegov 保留宿主测试注入桩)。 5. 构建脚本 `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 等)再次错配。