WebAssembly 模块检查器
WebAssembly 模块由八个字节的前导(魔数 \0asm 加版本号)开头,后面是一连串段,每段是一个 ID 字节、一个 LEB128 长度,以及恰好那么多字节。本工具在浏览器里按顺序走完这串字节,展示模块对自己的全部声明:版本号、按文件顺序排列的每个段及其字节大小、偏移和条目数、带完整类型的导入与导出、以页和字节两种单位给出的内存上下限、start 函数,以及末尾的自定义段。 打开二进制文件的常见理由是体积。多数构建里 code 段占了文件的绝大部分,但并非总是如此:Go 模块的 data 段常常超过一兆,内嵌资源的 emscripten 构建以数据为主,而调试构建把体积藏在完全不影响运行的自定义段里。导入是与宿主的契约:emscripten 要 env 和 wasi_snapshot_preview1,wasm-bindgen 按路径要它生成的 JavaScript 胶水文件,Go 要 gojs;只要有一项没有提供,实例化时就会抛出 LinkError。 在去掉符号的二进制里,两个自定义段比其他内容更有价值:name 段还原模块名和函数名,producers 段记录编译器和处理过该文件的工具。两者存在时都会在这里解码。 本工具无法告诉你模块做了什么。code 段只测量大小,从不反汇编,也不执行任何代码,因此只在函数体内出现的特性(例如 SIMD 运算或原子指令)除非同时出现在签名或限制标志里,否则不会被列出。列出的特性只包括字节编码本身能够证明的那些。
使用方法
- 把 .wasm 文件拖到方框里,或点击方框选择文件。文件只在页面内读取,不会上传。
- 先看段落表:它给出每个段的字节大小和占整个文件的比例,这就是「这个模块为什么有三兆」的答案。
- 把导入列表当作宿主必须履行的契约,逐项核对模块名、字段名、种类和确切的签名。
- 把导出列表当作可以从 JavaScript 调用的接口,参数和返回类型直接取自模块自己的 type 段。
- 在内存面板查看初始页数和最大页数(一页为 65,536 字节),并在自定义段里看看是否还保留着 name 或 producers 段。
常见问题
- 我的 .wasm 文件为什么这么大?
- 先看段落表。在常见的 Rust 或 C 构建里,code 段占文件的 70% 到 90%,这时唯一诚实的办法是减少代码:减少泛型实例化、去掉 panic 消息的格式化、跑一遍 wasm-opt -Oz。但构成常常出人意料。Go 会把运行时的静态数据编译进来,所以 data 段很大;内嵌文件的 emscripten 构建以数据为主;保留了 DWARF 调试信息的构建则把体积放在 .debug_info 一类的自定义段里,这些段在运行时完全用不到,正是 wasm-strip 会删掉的部分。
- 内存显示为「17..∞ 页」是什么意思?
- WebAssembly 的内存以 65,536 字节为一页计算,初始 17 页意味着实例化的那一刻就占用约 1.1 MB。第二个数字是声明的上限;没有上限时,模块可以增长到宿主允许的最大值,在 32 位 WebAssembly 中是 65,536 页,也就是 4 GiB。声明了上限并不代表节省内存,它只是宿主可以提前预留地址空间的天花板。配合线程使用的共享内存是唯一必须声明上限的情况。
- 为什么导入和导出的名字全是 a、b、c?
- 这是开启了优化和 Closure 的 emscripten 输出。导入名和导出名在二进制里是字符串,缩短它们能减小文件,随附的 JavaScript 胶水代码也会同样压缩。运行时没有任何损失,但二进制本身就读不懂了。把函数索引映射回源码名字的 name 自定义段会在同一步被删除。它若还在,本工具会解码;若不在,producers 段往往是唯一能说明文件出自哪条工具链的线索。
- 模块导入了 env 和 wasi_snapshot_preview1,我需要提供什么?
- 每一条导入都是宿主对象必须按模块名和字段名填上的位置,引擎在实例化时还会检查类型。env 是 emscripten 汇集 C 运行时回调的命名空间;wasi_snapshot_preview1 说明模块是针对 WASI 构建的,在浏览器里需要一层 WASI 垫片;gojs 是 Go 运行时的桥;而 ./xxx_bg.js 这样的路径表示 wasm-bindgen 在等待它生成的胶水模块。只要缺少一项或参数个数不符,WebAssembly.instantiate 就会抛出点名的 LinkError。
- 它会运行或反汇编模块吗?
- 都不会。它只解析模块的声明:段、类型、导入、导出、限制、start 函数索引和自定义段。函数体只统计数量和总大小,指令从不解码,也不会创建任何引擎,因此即使是恶意模块在这里也什么都做不了。代价是行为不可见:无法知道某个函数在算什么、哪些导入真的会被调用、模块运行时究竟用掉多少内存,也无法知道函数体里是否含有类型上看不出来的 SIMD 或原子指令。
- 既然不读代码,怎么能说模块用了引用类型或多返回值?
- 因为部分 MVP 之后的提案改变的是编码本身的形状,而不只是指令集。返回值多于一个的函数类型就是多返回值;签名里的 externref 或第二张表就是引用类型;内存限制标志的第三个比特是线程所需的共享位;data count 段以及被动而非主动的段只存在于批量内存提案中;签名里的 v128 则是 SIMD。这里只声明字节能够证明的部分,只在函数体里以操作码形式出现的特性一概不作断言,所以这份清单可能比模块实际用到的特性更短。
相关工具
GIF 结构检查器
打开 .gif 并逐块读取:每帧的延迟与处置方式、循环设置、调色板、透明索引,以及每一帧真正占用了多少字节。
EditorConfig 测试器
粘贴 .editorconfig 和一组文件路径,查看每个文件最终解析出的属性,以及每个值由哪一小节、哪一行决定。
.gitignore 测试器
粘贴 .gitignore 和一组路径,立刻看出哪些被忽略,以及究竟是哪一行做的决定,与 git check-ignore -v 的答案一致。
ELF 二进制检查器
在浏览器中打开 Linux 可执行文件、.so 或 .o:架构、依赖的共享库、构建 ID,以及是否启用 PIE、NX 和 RELRO。
字体文件检查器
打开 .ttf、.otf 或 .woff,读取真实字族名、字形与字符数量、版本、许可文本以及嵌入权限。
JSON Schema 校验器
在浏览器中用 JSON Schema 校验 JSON 文档:每条错误都给出 JSON 指针、对应行号和失败的关键字。