把 bytenode 编译的 .jsc 还原成 JavaScript。纯静态解析:
纯 Rust 单二进制,不需要打过补丁的 V8,也不需要 Node 运行时、不联网。
支持 Node 8.0.0 → 26.10.0(510 个发布 / 36 个 V8 minor),≈ 25k 份 .jsc 通过测试、0 失败
(31 条版本线 × 41 份用例,按 bytenode 1.7.0 的编译参数);两层优化(寄存器折叠 → swc 复制传播)
提升反编译源码的可读性。
English · 中文
目录:特性 · 快速上手 · 安装 · 用法 · CLI 参考 · 支持的版本 · 实现 · 验证 · 仓库结构 · 已知限制 · 参与贡献
--runtime 补上 __runtime 桩,反编译结果直接 node 可跑;--verify 再过一遍
node --check。JSCD_NO_OPT=1 可对照原样。ReferenceError 的标识符。$ cat hello.js
function greet(name) {
return "hello " + name;
}
console.log(greet("world"));
$ npm i -g bytenode && bytenode -c hello.js # 本文用 Node 20.20.2
$ xxd -l 48 hello.jsc
00000000: cc05 dec0 0bc2 e400 9200 0000 e521 2eaa .............!..
00000010: f002 0000 0000 0000 011c 5401 2006 a860 ..........T. ..`
00000020: 0000 0000 0600 0000 010c 4c60 0000 0000 ..........L`....
$ jscd hello.jsc
console.log(greet("world"));
function greet(a0) {
return "hello " + a0;
}
$ jscd hello.jsc --runtime > hello.out.js && node hello.out.js
hello world
开头四个字节是小端的 code cache magic(cc 05 de c0 即 0xc0de05cc);其余头部字段用
jscd info hello.jsc 能一并解出(样例见用法)。
上面的产物是两层优化跑完的样子。想看没优化的原样(V8 真实的寄存器搬运,一条一条),加
JSCD_NO_OPT=1。
Node 22+(V8 12.4+)上有些内建属性名存在只读堆里。行为矩阵覆盖到的 29 个 V8 minor,jscd 都
内置了名表(tables/ro_map_*)、自动还原 —— 但只在 macOS 上:只读堆的编号跟平台走,换系统
就得自己建(jscd ro-map 生成后 --ro-map 传入;JSCD_NO_RO_MAP=1 可关掉内嵌表)。
没有名表时这类名字落成 <ro0_…> 占位,绝不会给错名字。
macOS / Linux —— Homebrew
brew install ejfkdev/tap/jscd
Windows —— Scoop
scoop bucket add ejfkdev https://github.com/ejfkdev/scoop-bucket
scoop install jscd
不先加 bucket、直接按清单 URL 装也可以:
scoop install https://raw.githubusercontent.com/ejfkdev/scoop-bucket/main/bucket/jscd.json
scoop install ejfkdev/scoop-bucket/jscd不行:Scoop 的bucket/app语法是在已添加的 bucket 里找,不认owner/repo路径。
预编译产物 —— 每个 tag 的 Release 里都是裸可执行文件(Linux / macOS / Windows × x64 / arm64;Linux amd64 是静态 musl,Linux 与 Windows 走 UPX 压缩):
从 Release 页直接拿一个(不是压缩包、没有安装器 —— 文件本身就是可执行文件):
curl -fLO https://github.com/ejfkdev/jscd/releases/download/v0.1.0/jscd-v0.1.0-linux-amd64
chmod +x jscd-v0.1.0-linux-amd64
./jscd-v0.1.0-linux-amd64 --version
cargo(任何有 Rust 1.96+ 的平台)
cargo install jscd # 从 crates.io 源码构建
cargo binstall jscd # 或者:直接拉 release 里的可执行文件,不编译(cargo-binstall)
源码构建
cargo install --git https://github.com/ejfkdev/jscd # 直接装 main
git clone https://github.com/ejfkdev/jscd && cd jscd
cargo build --release # -> target/release/jscd
cargo install --path . # ……或把这份构建装进 ~/.cargo/bin
需要 Rust 1.96+(swc 优化器要求较新的 rustc),无系统依赖。
scripts/release.sh vX.Y.Z 先过发版门禁(cargo test --release、
clippy --all-targets -- -D warnings,以及端到端冒烟 scripts/ci_smoke.sh —— fixture 真编译成
.jsc → 反编译 → 语法门禁 + 整脚本行为对拍),再推一个带说明的 annotated tag;tag 说明就是
GitHub Release 的描述。tag 一推即触发 .github/workflows/release.yml,在上面那六个平台重建裸
二进制。之后 cargo publish 发到 crates.io(release.sh 保证 tag 与 Cargo.toml 版本一致),
再跑一次 python3 scripts/update_readme_help.py 刷新下面的 CLI 帮助;
Homebrew tap 与
Scoop bucket 每天自动更新时会带上新版本。
jscd app.jsc # 单文件反编译到 stdout
jscd app.jsc app.js # ……写到文件
jscd dist/ # dist/ 下所有 .jsc → dist-out/(保持层级)
jscd dist/ out/ # ……改写到 out/
jscd info app.jsc # 头部字段与识别出的 Node/V8 版本
jscd disasm --filter main app.jsc
jscd ro-map probe.jsc > m.json # 生成只读堆名表
jscd --help # 双语帮助(-h、`help`、`help <子命令>`)
.jsc 文件,或目录(递归找 *.jsc);输出默认:文件 → stdout,目录 → 输入同级的
<输入名>-out/(保持层级)—— -o / 位置参数 OUTPUT 可指定文件或目录,- 表示 stdout。jscd info 的 supported: yes|no
同理),不会静默吐一份只有运行时前导的文件。jscd ro-map 自己建一张再 --ro-map 传入(JSCD_NO_RO_MAP=1 可关掉内嵌表)。对不上的表会被忽略:
宁可留 <ro0_…> 占位,也不给错名字。JSCD_LANG(再依次看 LC_ALL / LC_MESSAGES / LANGUAGE / LANG / LC_CTYPE):
zh* 中文、其余英文,JSCD_LANG=zh|en 可强制。| 旗标 | 适用 | 作用 |
|---|---|---|
-o, --output <路径> | 全部形态 | 输出文件 / 目录 / -(与位置参数 OUTPUT 等价) |
--quiet | 目录输入 | 不打印逐文件进度 |
--json | 全部子命令 | 机器可读输出 |
--filter <子串> | disasm | 只输出名字含该子串的函数 |
--ro-map <路径> | decompile | 只读堆名表,由 jscd ro-map 生成 |
--verify | decompile | 语法门禁:产物过一遍 node --check(需要 PATH 里有 node) |
--runtime | decompile | 保留可运行前导(__runtime 占位实现 + 上下文变量提升 + 别名块)。默认不带;想让产物能 node 跑就加上 |
-h, --help | 全部形态 | 打印帮助(jscd help <子命令> 看单个命令) |
-v, -V, --version | 全部形态 | 打印名字、版本与仓库地址 |
--help 与 help <子命令> 的输出原样收在这里(刷新用 python3 scripts/update_readme_help.py)。
jscd --help 与每个子命令,原样输出jscd --help
$ jscd --help
jscd v0.1.0 — V8 代码缓存 → JavaScript
https://github.com/ejfkdev/jscd (MIT license)
把 bytenode 编译的 .jsc(V8 代码缓存)还原成 JavaScript。纯静态解析 ——
不需要打过补丁的 V8,也不需要 Node 运行时;目录输入会递归处理。
用法:jscd [选项] <输入> [输出] # 默认动作就是反编译
jscd <子命令> [参数...] # 分阶段分析
jscd help [子命令] | version | -h | -v | -V
输入 .jsc 文件,或目录(递归找 *.jsc)。
输出 文件、目录,或 `-` 表示 stdout:
<文件.js> 单个输入文件时写这个文件
<目录> 目录输入时写入该目录,保持层级
默认:文件输入 → stdout;目录输入 → 输入同级的 <输入名>-out/
选项:
-o, --output <路径> 输出文件 / 目录 / -(与位置参数 OUTPUT 等价)
--ro-map <路径> 只读堆名表(decompile 用,还原内建属性名)
--verify 用 `node --check` 实编译校验产物(decompile)
--runtime 保留可运行前导(`__runtime` 占位实现),产物能直接 `node` 跑;
默认**不带**,只输出还原出来的代码本身
--filter <子串> 只输出名字含该子串的函数(disasm)
--json 机器可读输出(info/strings/functions/disasm/ro-map)
--quiet 不打印逐文件进度(目录输入时)
-h, --help 打印本帮助
-v, -V, --version 打印名字、版本与仓库地址
JSCD_LANG=zh|en 强制帮助/报错的语言(默认自动识别)
子命令:
decompile 重建 JavaScript(默认动作,等价于直接 `jscd <输入>`)
info 头部字段与识别出的 Node/V8 版本
strings 常量池里的字符串与符号
functions SharedFunctionInfo 树(名字、形参、字节码长度)
disasm 字节码反汇编(View8 兼容文本)
ro-map 从探针缓存生成只读堆名表
help 打印帮助(可跟子命令名)
version 打印名字、版本与仓库地址