Skip to content

Commit 48c1fd1

Browse files
ziadziad
authored andcommitted
* fix some bugs, update readme
1 parent 650823e commit 48c1fd1

14 files changed

Lines changed: 657 additions & 154 deletions

README.md

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
[![CI](https://github.com/DevNamedZed/webasmjs/actions/workflows/ci.yml/badge.svg)](https://github.com/DevNamedZed/webasmjs/actions/workflows/ci.yml)
44
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
55

6-
TypeScript library for programmatically generating WebAssembly modules. Build WASM bytecode using a fluent builder API — define functions, memory, tables, globals, imports, and exports, then compile and instantiate at runtime.
6+
TypeScript library for building and analyzing WebAssembly modules. Generate WASM bytecode with a fluent builder API, or point it at any `.wasm` binary to disassemble, inspect, and decompile it.
77

88
[Playground](https://devnamedzed.github.io/webasmjs/) | [API Reference](docs/api.md) | [Getting Started](docs/getting-started.md) | [Examples](docs/examples.md)
99

1010
## Features
1111

12+
### Module Builder
13+
1214
- Fluent builder pattern for constructing WASM modules
1315
- **562 instructions** — arithmetic, control flow, memory, tables, globals, SIMD, atomics, GC, exception handling
1416
- **Target system**`mvp`, `2.0`, `3.0`, `latest` with automatic feature gating
@@ -21,9 +23,35 @@ TypeScript library for programmatically generating WebAssembly modules. Build WA
2123
- Function imports/exports with host interop
2224
- Memory and table management with import/export
2325
- WAT text format output and parsing
24-
- Binary reader for inspecting compiled modules
2526
- Compile-time verification (control flow + operand stack)
2627
- Data-driven — opcodes generated from `generator/opcodes.json`
28+
29+
### Binary Analysis
30+
31+
- **Binary reader** — parses all WASM sections: types, imports, functions, tables, memories, globals, exports, elements, data segments, tags, custom sections. Supports GC types (structs, arrays, recursive groups), SIMD, threads, memory64, and reference types.
32+
- **Disassembler** — generates WAT text format from binary, per-function or full-module
33+
- **Instruction decoder** — decodes all opcodes including prefixed families (GC, SIMD, bulk memory, threads) with offset and length tracking
34+
- **DWARF parser** — reads `.debug_info`, `.debug_abbrev`, `.debug_line`, `.debug_str` sections. Extracts function names, parameter/local variable names with WASM local indices, type information (structs, pointers, base types), struct field names, global variable addresses, and source file/line mappings. Supports DWARF 4 and 5.
35+
- **Source map parser** — V3 format with VLQ decoding, maps WASM byte offsets back to source files, lines, and columns
36+
- **Name demangling** — C++ (Itanium ABI) and Rust (v0 + legacy) symbol demangling
37+
38+
### Decompiler
39+
40+
Converts WASM functions into structured C-like pseudocode. The pipeline: decode instructions → build a control flow graph → convert to SSA → run optimization passes → compute dominance (Cooper-Harvey-Kennedy) → recover high-level control flow → lower to expression trees → emit output.
41+
42+
- **Control flow recovery** — if/else, while, do-while, for loops, switch/case from `br_table`, early returns, break/continue. Natural loop detection via dominance and post-dominance analysis.
43+
- **SSA optimization** — constant folding, copy propagation, dead code elimination, double negation elimination, comparison inversion, common subexpression elimination, phi simplification. Iterates until convergence.
44+
- **Stack frame removal** — detects the `__stack_pointer - N` shadow-stack pattern from LLVM/Emscripten, removes prologue/epilogue, cleans up all `__stack_pointer` references from output.
45+
- **Memory patterns** — variables with 2+ distinct constant offsets become struct field access (`ptr->field_0`). Expressions like `base + (index << 2)` become array indexing (`base[index]`). With DWARF info, fields get their real names.
46+
- **Type inference** — maps WASM types to C types, recognizes sub-word loads as casts (`load8_s``(byte)`, `load16_u``(ushort)`), integrates DWARF type information when present.
47+
- **Variable naming** — context-aware names from definition site (loads, calls, loop counters) and use site (addresses → `ptr`, comparisons → `cond`). 326 known functions (C stdlib, POSIX, WASI, Emscripten, Rust, Go, AssemblyScript) provide parameter and return value names.
48+
- **String literals** — resolves constant addresses against data segments and inlines the string content.
49+
- **Expression formatting** — operator precedence, parenthesization, strength reduction display (`x << 2``x * 4`), unsigned comparison casts, ternary operators from `select`.
50+
51+
Tested against real-world binaries — Quake (944 functions), Doom (769 functions), ioquake3, all decompiled successfully.
52+
53+
### General
54+
2755
- Zero production dependencies
2856

2957
## Install
@@ -165,7 +193,17 @@ See the [API Reference](docs/api.md) for complete documentation.
165193

166194
## Playground
167195

168-
Try webasmjs in the browser with the [interactive playground](https://devnamedzed.github.io/webasmjs/). It includes 100+ examples covering arithmetic, control flow, memory, tables, imports, floating point, i64/BigInt, SIMD, GC types, algorithms, WAT parsing, and post-MVP features.
196+
Try webasmjs in the browser with the [interactive playground](https://devnamedzed.github.io/webasmjs/). 100+ builder examples covering arithmetic, control flow, memory, tables, imports, floating point, i64/BigInt, SIMD, GC types, algorithms, WAT parsing, and post-MVP features.
197+
198+
Drop any `.wasm` file into the explorer to inspect it. The explorer provides:
199+
- Module structure — types, imports, functions, tables, memories, globals, exports, data segments
200+
- WAT disassembly and decompiled C-like output per function, with syntax highlighting
201+
- Hex view with instruction-colored bytes
202+
- Size analysis per section and per function
203+
- String extraction from data segments
204+
- Feature detection (which WASM proposals the binary uses)
205+
- Call graph, cyclomatic complexity, and dead code analysis
206+
- DWARF and source map integration for symbol recovery
169207

170208
To run the playground locally:
171209

@@ -192,15 +230,6 @@ npm run test:coverage
192230
npm run generate
193231
```
194232

195-
## Contributing
196-
197-
1. Fork the repository
198-
2. Create a feature branch (`git checkout -b feature/my-feature`)
199-
3. Make your changes and add tests
200-
4. Run `npm test` to verify
201-
5. Commit and push
202-
6. Open a pull request
203-
204233
## License
205234

206235
[MIT](LICENSE)

playground/WasmDecompiler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { ModuleInfo } from '../src/BinaryReader';
22
import { decompileFunction, createNameResolver } from '../src/decompiler/Decompiler';
33
import type { NameResolver, NameResolution } from '../src/decompiler/Decompiler';
4+
import type { FieldResolver } from '../src/decompiler/LoweredEmitter';
45
import type { DwarfDebugInfo, DwarfFunction } from '../src/DwarfParser';
56

6-
export type { NameResolver, NameResolution };
7+
export type { NameResolver, NameResolution, FieldResolver };
78
export { decompileFunction, createNameResolver };
89

910
export function createNameResolverWithDwarf(

playground/explorer.ts

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ function buildByteRanges(data: Uint8Array): ByteRangeMap {
100100
import { parseDwarfDebugInfo, getLineEntriesForAddressRange } from '../src/DwarfParser';
101101
import type { DwarfDebugInfo, DwarfLineInfo } from '../src/DwarfParser';
102102
import { decompileFunction, createNameResolver } from './WasmDecompiler';
103-
import type { NameResolver } from './WasmDecompiler';
103+
import type { NameResolver, FieldResolver } from './WasmDecompiler';
104104
import { parseSourceMap, lookupMapping, getSourceLine } from '../src/SourceMapParser';
105105
import type { ParsedSourceMap, SourceMapping } from '../src/SourceMapParser';
106106

@@ -782,6 +782,112 @@ export default class Explorer {
782782
return this.dwarfFunctionMap;
783783
}
784784

785+
private buildFieldResolver(funcGlobalIndex: number): FieldResolver | undefined {
786+
const dwarfData = this.getDwarfInfo();
787+
if (!dwarfData || dwarfData.types.size === 0) {
788+
return undefined;
789+
}
790+
791+
// Find the DWARF function for this global index
792+
const dwarfFunc = this.findDwarfFunctionByGlobalIndex(funcGlobalIndex);
793+
if (!dwarfFunc) {
794+
return undefined;
795+
}
796+
797+
// For each parameter with a pointer-to-struct type, build varName → field map
798+
const varFieldMaps = new Map<string, Map<number, string>>();
799+
800+
for (const param of dwarfFunc.parameters) {
801+
if (!param.name || param.typeOffset === null) {
802+
continue;
803+
}
804+
805+
const structFields = this.resolvePointerToStructFields(param.typeOffset, dwarfData.types);
806+
if (structFields && structFields.size > 0) {
807+
const paramName = param.name.replace(/[^a-zA-Z0-9_$]/g, '_');
808+
varFieldMaps.set(paramName, structFields);
809+
}
810+
}
811+
812+
// Also check local variables
813+
for (const variable of dwarfFunc.variables) {
814+
if (!variable.name || variable.typeOffset === null) {
815+
continue;
816+
}
817+
818+
const structFields = this.resolvePointerToStructFields(variable.typeOffset, dwarfData.types);
819+
if (structFields && structFields.size > 0) {
820+
const varName = variable.name.replace(/[^a-zA-Z0-9_$]/g, '_');
821+
if (!varFieldMaps.has(varName)) {
822+
varFieldMaps.set(varName, structFields);
823+
}
824+
}
825+
}
826+
827+
if (varFieldMaps.size === 0) {
828+
return undefined;
829+
}
830+
831+
return {
832+
resolveField(baseName: string, offset: number): string | null {
833+
const fieldMap = varFieldMaps.get(baseName);
834+
if (fieldMap) {
835+
return fieldMap.get(offset) || null;
836+
}
837+
return null;
838+
},
839+
};
840+
}
841+
842+
private resolvePointerToStructFields(typeOffset: number, types: Map<number, import('../src/DwarfParser').DwarfTypeInfo>): Map<number, string> | null {
843+
const typeInfo = types.get(typeOffset);
844+
if (!typeInfo) {
845+
return null;
846+
}
847+
848+
// Follow pointer → struct
849+
if (typeInfo.tag === 'pointer' && typeInfo.referencedType !== null) {
850+
return this.resolvePointerToStructFields(typeInfo.referencedType, types);
851+
}
852+
853+
// Follow typedef → target
854+
if (typeInfo.tag === 'typedef' && typeInfo.referencedType !== null) {
855+
return this.resolvePointerToStructFields(typeInfo.referencedType, types);
856+
}
857+
858+
// Follow const → target
859+
if (typeInfo.tag === 'const' && typeInfo.referencedType !== null) {
860+
return this.resolvePointerToStructFields(typeInfo.referencedType, types);
861+
}
862+
863+
// Struct with fields
864+
if (typeInfo.tag === 'struct' && typeInfo.fields && typeInfo.fields.length > 0) {
865+
const fieldMap = new Map<number, string>();
866+
for (const field of typeInfo.fields) {
867+
fieldMap.set(field.byteOffset, field.name);
868+
}
869+
return fieldMap;
870+
}
871+
872+
return null;
873+
}
874+
875+
private findDwarfFunctionByGlobalIndex(globalIndex: number): import('../src/DwarfParser').DwarfFunction | null {
876+
const dwarfFuncMap = this.getDwarfFunctionMap();
877+
if (!dwarfFuncMap) {
878+
return null;
879+
}
880+
const funcName = dwarfFuncMap.get(globalIndex);
881+
if (!funcName) {
882+
return null;
883+
}
884+
const dwarfData = this.getDwarfInfo();
885+
if (!dwarfData) {
886+
return null;
887+
}
888+
return dwarfData.functions.find(func => func.name === funcName) || null;
889+
}
890+
785891
private buildDwarfParameterTypeMap(): Map<number, Map<number, string>> | null {
786892
const dwarfData = this.getDwarfInfo();
787893
if (!dwarfData || dwarfData.functions.length === 0 || !this.moduleInfo) {
@@ -1171,6 +1277,18 @@ export default class Explorer {
11711277
return funcParams?.get(paramIndex) || null;
11721278
} : undefined,
11731279
);
1280+
1281+
// Add global variable address resolution from DWARF
1282+
const dwarfGlobalVars = this.dwarfInfo?.globalVariables;
1283+
if (dwarfGlobalVars && dwarfGlobalVars.length > 0) {
1284+
const globalAddrMap = new Map<number, string>();
1285+
for (const globalVar of dwarfGlobalVars) {
1286+
globalAddrMap.set(globalVar.address, globalVar.name);
1287+
}
1288+
this.nameResolver.resolveGlobalAddress = (address: number): string | null => {
1289+
return globalAddrMap.get(address) || null;
1290+
};
1291+
}
11741292
this.parsedSourceMap = null;
11751293
this.detectSourceMappingUrl();
11761294
this.renderExplorer();
@@ -2561,7 +2679,8 @@ export default class Explorer {
25612679

25622680
if (activeTab === 'decompiled') {
25632681
if (this.nameResolver && this.moduleInfo) {
2564-
const decompiledCode = decompileFunction(this.moduleInfo, funcIndex, this.nameResolver);
2682+
const globalFuncIdx = this.getImportedCount(0) + funcIndex;
2683+
const decompiledCode = decompileFunction(this.moduleInfo, funcIndex, this.nameResolver, this.buildFieldResolver(globalFuncIdx));
25652684

25662685
// Build function name → local index map for clickable calls
25672686
const funcNameMap = new Map<string, number>();

0 commit comments

Comments
 (0)