Files
MeowOS-SDK/sdk/CLOCK_ABI.md
T
AskaEth a07f9752d3 SDK: 内置 mipsel o32 时钟/睡眠 ABI 修复(meow_ui_time.c)+ 宿主单测
zig 0.17 内置 musl 在 mipsel 上头声明 64 位 time_t、实现走 time32 系统调用
(__ILP32__ 守卫),导致 meow_ui_time_ms() 毫秒位恒 0、高 32 位随机,
usleep(16000) 立即返回(帧节拍/轮询变忙等空转)。主仓已验证的修复逐字节
搬入公开仓:取时按内核 o32 ABI(8 字节 {int32 sec; int32 nsec})再升 64 位,
同 TU 强定义 __clock_gettime/64 与睡眠入口,链接期接管 libc。

- meow_ui_time.c / CLOCK_ABI.md:cp 搬运,md5 与主仓一致(含原注释与依据链)
- meow_ui_gfx.c:删掉 now_ms/meow_ui_time_ms(避免与 meow_ui_time.o 重复符号),
  6 处内部调用改外部 meow_ui_time_ms;<time.h> 不再使用
- meow_ui_internal.h:补 meow_time_ms_from_parts 声明
- build_sdk.sh:OBJS 首位加 meow_ui_time,头注释写明归档须排在 -lc 之前
- test_meow_time.c:适配公开仓构建方式,加负 sec 回绕两例,输出断言数(15)
- sdk/README.md:公开说明本次修复、链接顺序要求与已知限制(2038 前有效)
- tuku/test/run_tests.sh、run_webtoon_tests.sh:$SDK 源文件清单补 meow_ui_time.c
  (宿主集成测试直接编 SDK 源,否则链接期 undefined symbol)

验证:sdk/build_sdk.sh 0 warning;宿主 test_meow_time == PASS == checks=15;
tuku 两套测试全 PASS(run_tests 462+106+325 断言、webtoon 240 断言,0 failed);
探针/ picbrowser 反汇编确认 __clock_gettime 取自本 TU(8 字节 time32 + 升位),
无 libc VDSO 前导;picbrowser 仍为 MIPS32r2 静态 ELF。
2026-11-05 20:27:26 +08:00

9.3 KiB
Raw Blame History

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)

  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 等)再次错配。