Files
MeowOS-SDK/sdk/CLOCK_ABI.md
T
AskaEth 27a3750e36 SDK: 同步主仓 M15/M16/bug3/MD3 修复 — 时钟 ABI 补全/IME 冷却窗/列表行高派生/形状 token/翻页键
公开 SDK 落后主仓的一批 bugfix(主仓是固件构建源,这些修复已在设备上跑过)。
本次只做「主仓 → 公开仓」单向同步,图像栈方向由主仓侧另一单处理。

时钟 ABI(M16 第二弹,原文件自认「残余未修」):
- meow_ui_time.c 147→345 行:futex 相对超时接管(__timedwait/_cp)、
  timerfd_create/settime/gettime 的 itimerspec 64↔32 双向转换、
  __clock_settime64 越界返 ENOTSUP,另有 4 个纯换算函数
- meow_ui_internal.h:struct meow_time32 / meow_itimerspec32 契约移入内部头
- CLOCK_ABI.md:补接管机制、验证配方与 objdump 证据
- 新增 test_meow_time64.c

IME bug3(成对才有意义):
- meow_ui_ime.c:IME_TAP_COOLDOWN_MS 200 冷却窗 + 打开时二次 drain
- meow_ui_input.c:meow_input_touch_reset() 并在 drain 内调用(复位触摸状态机)
- test_ime_fix.c:新增用例④ stray-tap

列表行高(M15,修用户可见 bug):
- meow_ui_widget.c:listv_item_h() 由主题字体行高派生,不再写死 56
  (两行列表 62 > 56 会让首行标题被裁剪守卫整条跳过)
- include/meow_ui_widgets.h:新增 meow_listview_item_h()
- 新增 test_meow_ui_listview_rowh.c

其余:meow_select.c / meow_ui_dialog.c 统一 MD3 形状 token;
include/meow_ipc.h 补 MSG_KEY_PAGEUP/PAGEDOWN 枚举;
meow_ui_sys.c / meow_cachegov.c 取时统一走 meow_ui_time_ms()。

测试辅助函数标定:tuku/test/test_settings_lock_host.c 的注入前等待 150ms→350ms。
该等待按修复前 IME 标定,新冷却窗下首键 dt_open≈114ms 会被当迟到抬起吞掉,
只录到 "68" 两位。仅改等待时长,断言原样保留。

验证:tuku 四套脚本全绿(run_tests 644 PASS / zip 106 / cbz 296 / webtoon PASS);
12 个源文件与主仓逐字节一致;sdk/build_sdk.sh 34 成员;
tuku/build_tuku.sh 产出静态 MIPS32r2 ELF。
2026-11-06 21:31:54 +08:00

18 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 用于宿主/真机一眼验收。
  6. M16 续修同源入口(同一 TU,#if MEOW_TIME32_KERNEL_ABI 限定 mips o32): __timedwait_cp/__timedwait(futex 相对超时)、__timerfd_settime64/ __timerfd_gettime64/timerfd_create、__clock_settime64。换算逻辑抽成平台无关的 meow_time32_{rel,abs}_from_parts() / itimerspec 转换(meow_ui_internal.h), 宿主 test_meow_time64.c 直接覆盖。清单、逐项证据与未修项配方见 §6/§7。

非 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() 的修正也在同一份归档里生效(链接期覆盖)。
  • 同步等待(M16):pthread_cond_timedwait / sem_timedwait / pthread_join 超时 / aio_suspend / 非 PI 的 pthread_mutex_timedlock/rwlock timed 的相对超时按真实剩余 时间生效(不再「亚秒即超时」)。SDK 侧直接受益者是 meow_ui_audio.c 的 5 处条件等待 (:312/:351/:417/:431/:499)。 timerfd_*、clock_settime 一并修正(当前无调用方)。
  • 真机写入红线未触碰;本单只改 SDK 源与文档/测试。

6. 同源入口全量清点(M16)

M16 把 zig 0.17 musl 源里所有 #if defined(SYS_*_time64) && !defined(__ILP32__) 守卫 逐条编译、反汇编核对(zig cc --target=mipsel-linux-musleabi),结论如下。 「回退分支」= __ILP32__ 被定义时实际编译进去的代码。

6.1 受损(回退分支把 time64 结构直传 time32 系统调用)

musl 源(守卫行) 入口 回退分支实际行为 状态
thread/__timedwait.c:14 futex 相对超时 → pthread_cond_timedwait / sem_timedwait / pthread_join 超时 / aio_suspend / 非 PI pthread_mutex_timedlock / rwlock timed __syscall_cp(SYS_futex=4238, …, to),to 是 16B time64 结构;内核只读前 8B:tv_nsec 落在 tv_sec 高半字(恒 0)→ 亚秒等待变 0(立即 ETIMEDOUT)、整秒等待丢纳秒 M16 已接管
linux/timerfd.c:14 timerfd_settime 32B itimerspec 直传 SYS_timerfd_settime32=4323;内核按 16B 读 → value.tv_sec 读到调用者的 interval.tv_nsec,value.tv_nsec 读到填充字节 M16 已接管
linux/timerfd.c:42 timerfd_gettime 4322 只写 16B,调用者按 32B 解 → 字段串位 M16 已接管
time/clock_settime.c:9 clock_settime 16B 直传 SYS_clock_settime32=4262 → nsec 恒 0 M16 已接管
signal/sigtimedwait.c:10 sigtimedwait 16B 直传 SYS_rt_sigtimedwait=4197 未修(SDK/App 未用,§7.3 配方)
ipc/semtimedop.c:19 semtimedop 16B 直传 SYS_ipc=4117(IPCOP_semtimedop) 未修(未用)
time/timer_settime.c:13 timer_settime 32B itimerspec 直传 4258 未修(未用)
time/timer_gettime.c:11 timer_gettime 4259 只写 16B 未修(未用)
stat/utimensat.c:15 utimensat(futimens 同源) times[2](32B)直传 4316 未修(仅 ncm 宿主 bench/测试出现)
network/recvmmsg.c:21 recvmmsg 16B timeout 直传 4335 未修(未用)
mq/mq_timedsend.c:10 mq_timedsend 16B 直传 4273 未修(未用)
thread/pthread_mutex_timedlock.c:8 PI 互斥量的 timedlock 快路径 16B 直传 4238(FUTEX_LOCK_PI) 未修(非 PI 走已修的 __timedwait;SDK/App 不用 PI 互斥量)

唯一在用的受害者是 futex:meow_ui_audio.c(5 处,:312/:351/:417/:431/:499)、 meow_audiod、ncm 播放线程都用 pthread_cond_timedwait,修复前「剩 0.4s」会被内核当成 {0,0} 立即超时 → 音频环形缓冲 的等待退化成忙轮询。

6.2 不受影响(回退分支自己做了宽度转换)——纠正 M11 §6 的猜测

musl 源 证据
select/pselect.c:15 回退分支 syscall_cp(SYS_pselect6=4301, …, (long[]){s, ns}, …):o32 的 long=4B → 正好 8B time32
select/ppoll.c:14 同上(4302)。反汇编 __ppoll_time64:lw 0x0/0x8 读 16B 结构 → sw 成栈上两字 → addiu $4,$zero,0x10ce(4302)
select/select.c:28 mips 无 SYS_select,走 (long[]){s,ns} + 4301;同 pselect
linux/clock_adjtime.c:40 回退分支保留 if (sizeof(time_t) > sizeof(long)),构造 long 版 struct ktimex
misc/getrusage.c:9 同上宽度分支(dest = &ru_maxrss - 4*sizeof(long) + 回填)
linux/wait4.c:11 同上宽度分支

M11 §6 把 pselect/ppoll 列为「未验证」是保守估计;本次逐条反汇编确认它们本来就是对的, 不需要接管(也就没有可修的低风险项)。

7. 接管机制与未修项配方(M16)

7.1 符号名不是 API 名:_REDIR_TIME64 把调用点改了名

zig 的 alltypes.h 无条件 _REDIR_TIME64=1,头文件里每个受影响 API 都有一行 __REDIR(x, __x_time64)(generic-musl/*.h)。应用里写 timerfd_settime(...),编译出来 引用的是 __timerfd_settime64。所以接管必须定义重定向后的符号:

API 应用引用的符号 libc 该 .o 还定义(必须一并接管,否则重复定义)
timerfd_settime/gettime __timerfd_settime64 / __timerfd_gettime64 timerfd_create
clock_settime __clock_settime64 (无)
内部 __timedwait_cp __timedwait_cp __timedwait、__eintr_valid_flag(弱)

只定义 __timedwait_cp 是不行的:libc 的 __timedwait.o 会因 __timedwait 未解析被 拉入,与本文的重定义撞车。实测:

ld.lld: error: duplicate symbol: __timedwait_cp
>>> defined at ovr2.c
>>> defined at __timedwait.c:34 (.../musl/src/thread/__timedwait.c:34)

7.2 为什么链接期接管有效

  • 「hidden」不阻止静态链接接管:lld 按归档顺序解析,meow_ui_time.o 先于 libc.a 参与, libc 内部对 __timedwait_cp 的引用就绑到本文(实测 jal 0x…<__timedwait_cp>)。
  • 前提是 meow_ui_time.o 被拉入:SDK/App 都引用 meow_ui_time_ms(),且 clock_gettime 被重定向到 __clock_gettime64(本文也定义),所以它总会被拉入。若将来把本 TU 的 接管符号拆到没人引用的新 TU,接管会静默失效。
  • __eintr_valid_flag 给弱定义,libc signal/sigaction.c 的强定义优先(同时链 sigaction 实测不冲突)。
  • 取消点语义不重写:__timedwait_cp 仍调 libc 的 __syscall_cp,ECANCELED/EINTR 处理逐行照抄 musl。
  • 结构宽度前提:本 TU 与被接管入口的调用者必须用同一套 zig 头(time_t 8B、 struct timespec 16B)。当前四 App 与 launcher 都用 zig-mipsel-cc 编译 ($SYSROOT/lib 只有 libz.a,libc 来自 zig 内置 musl),前提成立;若将来换链, 必须整链替换、不能混用。

7.3 未修项的配方(同一模式,约 10 行/项)

  1. 在 meow_ui_time.c 的 mips 分支里定义 __<api>_time64(或该 API 的重定向名);
  2. 入参展成 meow_time64_spec / meow_time64_itimerspec,调 meow_time32_rel_from_parts()(超时类,饱和)或 meow_time32_abs_from_parts() (绝对时刻类,越界回 ENOTSUP);
  3. 发 SYS_<api>_time32(或 SYS_<api> 的 time32 号),输出再反向展开;
  4. 同一 .o 里 libc 定义的所有符号一起接管(见 7.1 表),否则重复定义;
  5. 宿主单测补边界,交叉反汇编确认结构宽度与 syscall 号。

不建议顺手全改:这些 API 当前零调用(全仓 grep 只有 ncm 的宿主 bench/测试用 utimensat), 每多接管一个就多一份「转换写错 → 静默写坏文件时间/信号等待」的面;优先级应按「谁先被 App 用」来定。根治仍是上游:换容器旧链(gcc 不定义 __ILP32__)编 SDK,或修补 zig 内置 musl 的 !__ILP32__ 守卫(本意只排除 x32)。

8. M16 验证

宿主(aarch64 Termux,gcc):

cd patch_v2/sdk
gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o /tmp/m.o     # 0 warning
gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time64.c /tmp/m.o -o /tmp/t && /tmp/t
gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c   /tmp/m.o -o /tmp/t0 && /tmp/t0
  • test_meow_time64(新,23 项):相对/绝对换算边界(INT32_MAX±1、INT64_MAX、负值)、 itimerspec 四字段不串位与越界原子性,以及「同一块 16B 内存按内核 o32 只读 8B」的 before/after 模拟(0.4s → {0,0} 立即超时;2.3s → 2s 整)。
  • test_meow_time(M11 回归):换算边界 + 单调前进,仍 PASS。

交叉:

bash patch_v2/sdk/build_sdk_host.sh --verify    # 76 成员、全 MIPS32r2、0 warning

反汇编(本单新增/改写的入口):

符号 syscall 号 缓冲宽度证据
__timedwait_cp 0x108e = 4238 SYS_futex 超时在栈上 8B(addiu $18,$sp,0x20 + sw $18,0x10($sp)),不再直传 16B 结构
__timerfd_settime64 0x10e3 = 4323 timerfd_settime32 从调用者 32B 结构读 0x0/0x4/0x8/0x10/0x14/0x18,打包成栈上 16B
__timerfd_gettime64 0x10e2 = 4322 内核只写栈上 16B,再按字段展开到调用者 32B
__clock_settime64 0x10a6 = 4262 clock_settime32 8B {int32 sec, int32 nsec}

链接期(真实 meow_ui.a + 探针,顺序同 App:probe.o meow_ui.a … -lc):

llvm-objdump -d probe_sdk | sed -n '/<__pthread_cond_timedwait_time64>:/,/^$/p' | grep jal.__timedwait
#   cd930: jal 0x419e8 <__timedwait_cp>    ← 绑到 meow_ui.a 里的实现
llvm-nm probe_sdk | grep -c __timedwait_cp # 1(libc 那份未被拉入)

真机验证仍待设备(本单未碰真机):pthread_cond_timedwait 传 100ms 超时的等待应真的等 约 100ms 再超时,而不是立即返回。