exe32_js — Windows 32-bit JavaScript simulator
A pure-JavaScript port of the Ruby exe32_rb project — runs
entirely in your browser, no server, no WASM, no toolchain. It parses
real Windows 32-bit (PE32) EXE files, decodes and executes a substantial
i386 instruction subset, resolves IAT imports against stubbed Win32 DLLs
(kernel32, user32, msvcrt), and — uniquely — visualises the engine’s
own class invocations and data flow live as the program runs.
The default boot image is an interactive Snake game built from a real
hand-assembled PE32 binary: the binary is a tight infinite x86 loop calling
kernel32!WriteConsoleA on a shared memory buffer at 0x00500000; the
game state, AI, and keyboard input live in JS and write the next frame
into that buffer on every tick. You can also drag-drop or upload your own
.exe.
Lineage
exe32_js is the in-browser JavaScript port of exe32_rb, a Ruby Windows-32
EXE emulator. The class taxonomy mirrors the Ruby parent so the engine reads
the same shape:
Loader/ DosHeader · NtHeaders · FileHeader · OptionalHeader32 ·
SectionHeader · ImportDescriptor · DataDirectory · ImageLoader
Memory/ VirtualSpace · Region · Heap · (Reader/Writer inlined)
CPU/ Registers · Decoder · Executor · ModRM/Operand (inlined)
Win32/ ApiDispatch · Handle · Kernel32 · User32 · Msvcrt
Process/ Process · PEB · TEB · CallStack · DynamicLinker · ConsoleOutput
…and the per-field offsets / x86 opcode handlers / flag semantics are verified against Microsoft’s PE/COFF spec and Intel SDM Vol. 2 directly.
The advantage of the JS port: zero install. Open the page, the simulator boots a real Windows EXE in the browser tab.
Five views
| View | Shows |
|---|---|
| Overview | Image summary + Imports/IAT + the running program’s console (full-height when Snake is loaded) + API call log + current instruction + registers — single dashboard |
| PE | Full IMAGE_* header tree (DOS, NT, FileHeader, OptionalHeader32, all 16 DataDirectories, every section, imports per DLL) + raw file hex |
| Execute | Disassembly walking forward from EIP + Registers/Flags + Stack with ESP/EBP markers |
| Memory | Virtual memory region map (Headers / .text / Stack / Heap / PEB / TEB / API_Stubs / SnakeBuffer) + hex view that jumps to any address |
| Classes | Live class-invocation lattice (every engine class as a node, edges pulse on caller→callee) + scrolling method call sequence |
Default app: Snake
The simulator boots into an interactive Snake game. Auto-play is on by default; the AI is a greedy chase (move toward food, avoid walls and self).
| Key | Action |
|---|---|
P | Toggle keyboard control ↔ auto-play |
W A S D / arrow keys | Steer (when keyboard mode is on) |
Enter | Restart after game over |
[Restart] button | Same, clickable |
The Snake “binary” is a real PE32 with a single import (kernel32 ⇒
GetStdHandle, WriteConsoleA, ExitProcess) and ~26 bytes of x86 code
in a tight infinite loop:
push -11 ; STD_OUTPUT_HANDLE
call [GetStdHandle]
mov ebx, eax ; save handle
.loop:
push 0
push 0
mov eax, [0x00500000] ; length
push eax
mov eax, 0x00500008 ; buffer
push eax
push ebx
call [WriteConsoleA]
jmp .loop ; never exits
JS owns the game logic and writes the next ASCII frame (with an ANSI
\x1b[2J\x1b[H clear prefix) to 0x00500000 + 8 on every tick. The
emulator’s ConsoleOutput.append recognises the clear sequence and wipes
its buffer, so the panel becomes a real animated terminal.
Other controls
| Key | Action |
|---|---|
space | Run / pause |
s | Step one instruction |
shift+s | Step ×10 |
r | Reset (restarts current image) |
v | Cycle views |
Upload .exe button | Pick a PE32 file from disk |
Drag-drop a .exe anywhere | Same as upload |
Hello World button | Load the alternative embedded demo (MessageBoxA + ExitProcess) |
Trace example
When the simulator steps a single instruction, the live trace looks like:
Executor.step → Decoder.decode → VirtualSpace.read8 (×3)
→ Executor.readOp → VirtualSpace.read32
→ Executor.writeOp → VirtualSpace.write32
→ ApiDispatch.maybeDispatch
→ Kernel32.WriteConsoleA → ConsoleOutput.append
…and each arrow lights up on the Classes view.
What’s implemented
- PE/COFF parsing — full DOS header, NT header, FileHeader, OptionalHeader32, all 16 data directories, section headers, full import descriptor + ILT/IAT walk.
- Memory — 4 GB sparse virtual space with 4 KB pages, per-region R/W/X bits, helpers for C / W string reads, simple first-fit heap.
- CPU — i386 instruction subset wide enough for typical Hello World
builds and the Snake-binary loop:
mov(every encoding),push/pop,lea,call/ret,jmp/jcc(all 16 conditions, short + near),add/sub/xor/and/or/cmp/test,inc/dec, Group 1 (80/81/83), Group 5 (FF/4),int,hlt,cdq,xchg,movzx,leave. Real ModR/M + SIB + disp decode. Real EFLAGS (CF/PF/AF/ZF/SF/DF/OF). - Win32 stubs —
kernel32:ExitProcess,WriteFile/WriteConsoleA/WriteConsoleW,GetStdHandle,GetLastError/SetLastError,GetCommandLineA,GetVersion,GetTickCount,GetModuleHandleA,GetProcAddress,LoadLibraryA,HeapCreate/GetProcessHeap/HeapAlloc/HeapFree,VirtualAlloc/VirtualFree,Sleep,GetCurrentProcessId/ThreadId,CloseHandle,CreateFileAuser32:MessageBoxA,MessageBoxW,MessageBoxExAmsvcrt:printf(with%d %u %x %s %cformat),puts,putchar,exit,malloc,free,strlen
- PEB / TEB — laid out at allocated bases, with
ImageBase,StackBase,StackLimit,ProcessEnvironmentBlockpointers wired up. - Dynamic linker — every IAT slot is rewritten at load time to point
at a synthetic stub address; when EIP hits a stub, the dispatcher
intercepts, runs the JS handler, pops stdcall args, sets
EAX, and returns to the caller — exactly like the real Windows loader.
Limitations (v1)
- PE32+ (64-bit) — rejected with a friendly error.
- .NET / CLR images (COM_DESCRIPTOR present) — rejected.
- Packed binaries (UPX, ASPack, etc.) — the loader works, but the unpacker stub usually uses SSE / FPU instructions outside the v1 subset and traps.
- No real GUI — only
MessageBoxis rendered visually;CreateWindowEx/ message loop is logged but not rendered as a real window. - Single thread —
CreateThreadis stubbed but doesn’t actually spawn. - No SEH chain unwinding (the
fs:[0]exception list is readable but exceptions aren’t dispatched). - No FPU / SSE / AVX instructions in v1.
How it works under the hood
The simulator is a single HTML file with embedded CSS and JavaScript — no
build, no server, no WASM, no toolchain. Everything runs in the browser tab.
The engine is split into clean class layers (see “Lineage” above); the
visualizer subscribes by reading window.proc and rendering each frame from
the live state.
The class-invocation instrumentation works by wrapping every method in every
engine class’s prototype at boot with a small shim that records a (caller, callee, method) event into a sliding buffer. The Classes view reads
that buffer to draw the lattice and the live sequence ribbon. Toggle to
Classes and step through the embedded Hello World (or pause Snake and
step) to see the structure emerge.