让 HarmonyOS PC 上 Harmonybrew 的 node 摆脱
--jitless,恢复 V8 真实 JIT, 并且交互式 TUI(kimi 等)全速可用。 实测:热循环稳态 约 9 倍提速(5–10ms vs 80–110ms),MAGLEV 编译并执行; 页面翻转开销从 ~60–80ms/页 降到 ~1ms/页,键入无卡顿。 设备:MOR-W52 / HarmonyOS 6.1.0.135 / HongMeng Kernel 1.12(底座 Linux 5.10) 日期:2026-08-10
鸿蒙 PC 的 HiShell 沙箱强制 XPM 代码签名校验,任何进程无法通过
mmap(PROT_EXEC) / mprotect(...→RX) 创建可执行内存。V8 的 JIT 需要 RWX
代码页,因此被卡死,Harmonybrew 只能给 node 打 --jitless 纯解释器。
wxjit 是一个 LD_PRELOAD shim,不改 node/V8 一行代码,用用户态手段恢复 JIT:
- 拦截
mmap/mprotect/munmap,接管 V8 对代码段的 RWX 请求(落地为 RW)。 - V8 执行到 RW 代码页触发 SIGSEGV(W^X 在取指层强制)。
- shim 在信号处理里:把该页代码补丁进预签名 ELF 模板 → 进程内自签名
→
dlopen让 ld-musl 建立合法 RX 映射 →mremap搬回 V8 期望的原地址 → 重新执行。 - V8 后续要写该页时触发写 SIGSEGV,shim 把页换回 RW(memfd 预填内容,消除零页竞态), 写完再翻回 RX。
旧版每次 RW→RX 翻转都要 fork+exec binary-sign-tool(~60–80ms/页),
批处理负载没问题,但 TUI/REPL 里 V8 持续打补丁,每次按键都在为签名付费,
交互不可用,只能回退 --jitless。
本版(fast edition)解决了两点:
- 进程内签名:
binary-sign-tool sign -selfSign 1被完整逆向并重实现 (见第 6 节)。自签名的"签名"只是 fs-verity descriptor 的 SHA-256, 无任何非对称密钥,进程内算一次不到 0.1ms。翻转总开销 ~1ms/页。 模板缺.codesign段时自动回退外部工具(需binary-sign-tool可用)。 - 关闭并发撕裂竞态:旧版靠 ~80ms 签名延迟"碰巧"躲过 V8 并发编译器 在快照与 mremap 之间写页的问题(实测快签名下 20% 崩溃率:SIGILL 死循环 → SIGSEGV)。本版翻转期间把页面降级为 PROT_READ 并把 pagemap 置为 状态 3(翻转中),任何并发写/执行都会立即缺页并排队等待,从结构上 消除了撕裂快照。压测 20/20 通过(修复前 8/20 失败)。
| 文件 | 说明 |
|---|---|
src/wxjit.c |
shim 源码(fast edition,LD_PRELOAD 库本体) |
prebuilt/wxjit.so |
预编译 shim(aarch64-linux-ohos,已自签) |
tools/gen_templates.sh |
生成 4 档 RX 代码池模板(64K/256K/1M/4M)到 $HOME/.tmp/wxjit |
tools/selfsign.py |
自签名算法的 Python 参考实现(与官方工具输出字节级一致) |
tests/test_shim.c |
独立自测(不依赖 node,验证翻转机制) |
tests/rxread.c |
诊断:验证 RX 页读回内容与 RW 恢复一致性 |
tests/bench.js / tests/bench2.js |
性能对比脚本 |
tests/kimi_pty_test.py |
TUI 交互延迟测试(PTY 内驱动 kimi 键入并测回显延迟) |
prebuilt/test_shim / prebuilt/rxread |
预编译测试二进制(install.sh 会在设备上重新编译) |
install.sh |
一键安装(见下) |
前置条件:目标机器已装 Harmonybrew + openssh(能 ssh ohos@设备 -p 8022),
并装有 ohos-sdk(提供 clang/lld;brew install node 时通常会带上)。
# 主机侧
scp -P 8022 -r ohos-node-jit-patch ohos@127.0.0.1:/storage/Users/currentUser/Download/
# 设备侧
cd ~/Download/ohos-node-jit-patch
zsh install.shinstall.sh 做的事:生成模板到 $HOME/.tmp/wxjit → 编译 shim(失败则用预编译)
→ 跑 test_shim + rxread 自测 → 有 node 则跑 bench.js → 输出启用方法。
shim 不依赖任何硬编码用户路径:模板目录默认 $HOME/.tmp/wxjit
(可用 WXJIT_DIR 覆盖),外部签名工具默认 $HOME/.harmonybrew/bin/binary-sign-tool
(可用 WXJIT_SIGN_TOOL 覆盖,仅回退路径用)。
# 方法 A:临时,单条命令
NODE_OPTIONS= LD_PRELOAD=~/.tmp/wxjit/wxjit.so node yourscript.js
# 方法 B:固化进 ~/.zshrc(覆盖交互 shell 的 node 与 kimi)
export LD_PRELOAD=$HOME/.tmp/wxjit/wxjit.so
export NODE_OPTIONS= # 删除/清空原有的 --jitless注意:改动前已打开的终端仍带旧环境变量,需重开终端生效。
验证 JIT 编译:
node --trace-opt -e 'function h(n){let a=0;for(let i=0;i<n;i++)a=(a+i*i)^(i*3);return a;}h(2000000);console.log("ok")'
# 看到 "[completed compiling ... (target MAGLEV) ...]" 即 JIT 已生效性能对比:
NODE_OPTIONS=--jitless LD_PRELOAD= node tests/bench2.js # 基线(解释器)
NODE_OPTIONS= LD_PRELOAD=~/.tmp/wxjit/wxjit.so node tests/bench2.js # JITTUI 交互验证(需要已安装 kimi):
python3 tests/kimi_pty_test.py ~/.tmp/wxjit/wxjit.so 40
# PTY-TEST PASS;本机实测 median 0ms / p90 34ms / max 291ms(40 次键入)| 变量 | 作用 |
|---|---|
WXJIT_DBG |
设为非空即在 stderr 输出完整翻转日志([t] exec-flip 等);不设则完全安静(TUI 安全) |
WXJIT_DIR |
模板/临时文件目录,默认 $HOME/.tmp/wxjit |
WXJIT_SIGN_TOOL |
外部签名工具路径(仅模板无 .codesign 段的回退路径用),默认 $HOME/.harmonybrew/bin/binary-sign-tool |
罕见错误事件(翻转失败、越界 SEGV、SIGILL 详情)始终追加到
$WXJIT_DIR/fault.log,不污染 stderr。该文件不存在 = 无错误事件。
逆向自 binary-sign-tool sign -selfSign 1,并经字节级比对验证
(4 档模板 × 随机内容,输出与官方工具逐字节一致)。参考:
鸿蒙PC 底层开发技术详解(七):二进制自签名算法的实现。
- ohos-sdk 的 lld 链接产物自带 4K 全零
.codesign段(页对齐)。 - 签名 = 填充该段前 296 字节:
ElfSignInfo头 8B(type=1, length=288)- fs-verity descriptor 256B + "signature" 32B。
- descriptor 关键字段:version=1, hashAlgorithm=1(SHA-256), logBlockSize=12, signSize(算摘要时 0,落盘时 32), dataSize=文件总大小, rootHash=Merkle 树根, flags=0x10(FLAG_SELF_SIGN), csVersion=3(@offset 255)。
- "signature" = SHA-256(descriptor with signSize=0)。无私钥、无证书。
- Merkle 树:文件按 4K 分页(末页补零),
.codesign所在页叶哈希置零, 其余页 SHA-256;每 128 个哈希(4K)聚一层;根页 ≤4K 时对其补零后再哈希。 - 官方工具签名前会把 PT_GNU_RELRO 程序头的 p_align 改成 8(参与哈希), 本实现同样处理。
- 段内 296B 之后全零即可(官方工具实测也不写 Merkle tree bytes)。
验签失败排查:hilog -t kmsg | grep -E "xpm|unsigned file"。
- 冷启动:JIT 编译本身有开销,首轮含翻转约 40–60ms;短任务可能不如 jitless,长跑/TUI/服务型负载收益大(稳态 9x)。
- node 退出时可能打一条无关紧要的段错误(musl 清理被搬移的映射), 不影响退出码与结果。
- 仅适用 Harmonybrew 用户态 node;对系统 ArkCompiler 无效也无必要。
- 旧终端记得重开;
WXJIT_DBG=1可用于诊断但不要在 TUI 里开(会刷屏)。
test_shim打印HARNESS PASS→ 翻转机制 OKrxread打印RXREAD DONE且两行 OK → RX 读回/RW 恢复一致node --trace-opt出现completed compiling ... MAGLEV→ JIT 编译 OKbench2.js后几轮显著低于 jitless 基线 → JIT 执行 OKpython3 tests/kimi_pty_test.py ~/.tmp/wxjit/wxjit.so→ TUI 交互 OK
| 模式 | bench.js 5 轮 (ms) | 说明 |
|---|---|---|
--jitless |
81 / 83 / 99 / 111 / 105 | 解释执行基线 |
| 旧版 wxjit(外部签名) | 261 / 6 / 7 / 6 / 5 | 稳态同 JIT,但每次翻转 60–80ms,TUI 卡顿不可用 |
| 本版 wxjit | 41–62 / ~10 / ~10 / 5–7 / 6–9 | 翻转 ~1ms,TUI 流畅;20/20 压测无崩溃 |