How I built a web based CPU Simulator
As someone passionate about computer engineering, reverse engineering, and system internals, I’ve always been fascinated by what happens “under the hood” of a computer. Not just at the application level—I mean the raw, low-level stuff. How does a MOV RAX, 0x10 actually change the state of the machine? What happens to the stack when you PUSH a value? How does the instruction pointer know where to go next?
This curiosity led me to develop a CPU simulator—a web-based project designed to visualize the internal workings of a CPU, from register operations to memory manipulations, similar to how tools like GDB work in a Linux environment.
In this blog post, I’ll walk you through the motivation, features, architecture, and how I built the whole thing from scratch.
Check it out live at https://cpusimulator.org
Why a web based simulator? Isn’t GDB Enough?
So here’s the thing. During my studies and exploration in reverse engineering, penetration testing, and low-level programming, I found that many learners struggle to understand how a CPU actually processes instructions. I was one of them at some point. Most theoretical resources are abstract—you read about registers and memory in a textbook, but you never really see what’s happening.
And while tools like GDB and IDA Pro are powerful, they can be overwhelming for beginners. Imagine someone who just learned what a register is, and you throw them into GDB with all its commands, flags, and cryptic output. That’s not a learning experience—that’s a survival test.
So I thought—what if I could build an interactive, visual, and beginner-friendly tool that simulates how a CPU executes assembly instructions step-by-step? Something you can open in your browser, type some assembly, and watch the CPU state change in real time?
That’s where the idea was born.
Understanding the CPU at a High Level
Before diving into the implementation, let’s take a step back and understand what we’re actually simulating here.
A CPU, at its core, is a machine that follows a simple cycle—Fetch, Decode, Execute.
- Fetch — The CPU reads the next instruction from memory, pointed to by the Instruction Pointer (RIP in x64).
- Decode — It figures out what the instruction means. Is it a
MOV? AnADD? AJMP? What are the operands? - Execute — It performs the operation. Move a value into a register, add two numbers, jump to a different address—whatever the instruction says.
This cycle repeats over and over, billions of times per second on a real CPU. In our simulator, we slow it down so you can observe each step, inspect every register, and watch memory change byte by byte.
Think of it like a slow-motion replay of a football match. The game happens too fast in real time, but when you slow it down, you see every move, every decision, every impact. That’s exactly what this simulator does for CPU execution.
What the Simulator Does
My CPU simulator is designed to mimic the operation of a simplified Intel x64 architecture. Let me walk you through the core features.
Instruction Parsing
The simulator can parse and execute x86 assembly instructions such as MOV, PUSH, POP, ADD, SUB, JMP, and more. It supports both register-to-register, register-to-memory, and immediate value operations.
Under the hood, each instruction goes through a parser that breaks it down into an opcode and operands. Here’s a simplified look at how instruction parsing works:
interface Instruction {
opcode: string;
operands: Operand[];
}
interface Operand {
type: "register" | "immediate" | "memory";
value: string | number;
}
When you type something like MOV RAX, 0x10, the parser breaks it down into:
Opcode: MOV
Operand1: { type: "register", value: "RAX" }
Operand2: { type: "immediate", value: 16 }
This structured representation makes it easy to route each instruction to the correct handler.
Register Simulation
The simulator models general-purpose registers like RAX, RBX, RCX, RDX, RSI, RDI, RSP, RBP, and the instruction pointer RIP. The user can view register values in real time as instructions are executed.
Each register is stored as a 64-bit value, and the simulator also handles sub-register access. So when you reference EAX, it knows you’re talking about the lower 32 bits of RAX. Same goes for AX (lower 16 bits), AH (bits 8-15), and AL (lower 8 bits).
RAX: | 63 .............. 32 | 31 ............ 16 | 15 .... 8 | 7 ..... 0 |
| (upper 32) | EAX | | |
| | | AH | AL |
| | | AX |
This level of detail matters because in real reverse engineering scenarios, you’ll frequently see instructions that operate on different register sizes, and understanding how they overlap is crucial.
Memory Viewer
This is one of my favorite parts. The memory component allows users to visualize memory content similar to how GDB displays it. Each memory block shows:
- Address — The hex address of the memory location
- Byte-level breakdown — Individual bytes displayed in hex
- Full word — The complete 64-bit value in hex
If you’ve ever used x/16xb $rsp in GDB to examine the stack, you’ll feel right at home. The difference is, here you can see it update in real time as each instruction executes, with visual highlights showing what just changed.
Operand Handling
The parser supports a variety of operand types:
- Immediate values — Direct numeric values like
MOV RAX, 0x10 - Registers — Register references like
MOV RAX, RBX - Memory references — Memory addressing like
MOV RAX, [0x200] - Register-indirect — Using a register as a pointer like
MOV RAX, [RBX]
Each operand type has its own resolution logic. For memory references, we calculate the effective address and read/write from the simulated memory space. For registers, we look up the current value from the register file.
Stack Behavior
The simulator models stack operations accurately, including PUSH and POP, updating the stack pointer (RSP) and memory layout accordingly.
When you execute PUSH RAX:
RSPis decremented by 8 (since we’re in 64-bit mode)- The value of
RAXis written to the memory address pointed to by the newRSP
And POP RBX does the reverse:
- The value at the memory address pointed to by
RSPis read intoRBX RSPis incremented by 8
You can watch this happen step by step in the simulator—the stack pointer moves, memory gets written, and the register updates. It makes the abstract concept of “the stack grows downward” suddenly very concrete.
Instruction Flow Control
Users can step through each instruction one at a time, watch how the CPU state changes, and debug their assembly-like programs interactively. The instruction pointer (RIP) advances after each step, and conditional jumps update it based on the flags register.
This is where the magic happens for learning. Instead of running a program and looking at the final output, you see every single state transition. It’s like having a magnifying glass on the CPU.
The Architecture
Let’s get into the technical side of how this thing is built.
Why TypeScript?
You might wonder why I used TypeScript — a web language — rather than something lower-level like C or Rust. The reason is simple: visualization and accessibility.
- The entire simulation runs in the browser—no installation required. Just open the URL and start writing assembly.
- TypeScript’s type system helped me model CPU components cleanly—registers, memory, instructions—all with proper type safety.
- The component-based architecture made it easy to modularize elements like the memory viewer, instruction editor, and register display.
While TypeScript is not optimal for performance-heavy tasks, for educational purposes and visualization, it’s a great fit. We’re not trying to compete with QEMU here—we’re trying to help people understand what a CPU does.
Project Structure
The project is built with Vite as the bundler and uses Tailwind CSS for styling. Here’s how the codebase is organized at a high level:
cpu-simulator/
├── src/
│ ├── core/ # CPU logic - registers, memory, ALU
│ ├── parser/ # Instruction parsing and decoding
│ ├── ui/ # Frontend components
│ └── utils/ # Helper functions
├── public/ # Static assets
├── index.html # Entry point
├── vite.config.ts # Build configuration
└── tailwind.config.js # Styling
The core module is the heart of the simulator. It contains the CPU state machine, the register file, and the memory model. The parser module handles converting raw assembly text into structured instruction objects. And the ui module renders everything to the screen.
The Execution Pipeline
Here’s how a single instruction flows through the system:
- Input — User types
MOV RAX, 0x10in the editor - Tokenization — The raw string is split into tokens:
["MOV", "RAX", ",", "0x10"] - Parsing — Tokens are analyzed to create an
Instructionobject with the opcode and typed operands - Validation — The instruction is checked for valid operand combinations (you can’t
MOVan immediate into an immediate, for example) - Execution — The instruction handler for
MOVis called, which resolves the source operand (0x10=16) and writes it to the destination (RAX) - State Update — The register display updates to show
RAX = 0x0000000000000010, andRIPadvances to the next instruction - Render — The UI re-renders to reflect the new CPU state
Each step is discrete and observable. That’s the beauty of building it this way—nothing happens in a black box.
A Quick Demo
Let me walk you through a simple example. Say you want to understand how function prologues work in x64. You’d type something like this into the simulator:
MOV RAX, 0x1
MOV RBX, 0x2
ADD RAX, RBX
PUSH RAX
POP RCX
Now, as you step through each instruction:
| Step | Instruction | RAX | RBX | RCX | RSP | Notes |
|---|---|---|---|---|---|---|
| 1 | MOV RAX, 0x1 | 0x1 | 0x0 | 0x0 | 0x7FF0 | RAX gets the value 1 |
| 2 | MOV RBX, 0x2 | 0x1 | 0x2 | 0x0 | 0x7FF0 | RBX gets the value 2 |
| 3 | ADD RAX, RBX | 0x3 | 0x2 | 0x0 | 0x7FF0 | RAX = RAX + RBX = 3 |
| 4 | PUSH RAX | 0x3 | 0x2 | 0x0 | 0x7FE8 | RSP decrements, value 3 pushed |
| 5 | POP RCX | 0x3 | 0x2 | 0x3 | 0x7FF0 | Value 3 popped into RCX, RSP restores |
See how each step changes the CPU state? You can trace exactly what happened, where values moved, and how the stack pointer shifted. This kind of visibility is invaluable when you’re learning assembly or debugging shellcode.
Who Is This For?
This project serves a variety of audiences:
-
Students learning computer architecture See how each instruction impacts the CPU state in real time. No more imagining what happens—you can watch it.
-
Reverse engineering enthusiasts Visualize how stack and memory manipulation work at the assembly level. Understanding stack frames, function calls, and register conventions becomes much easier when you can step through them visually.
-
Security researchers Understand memory vulnerabilities like buffer overflows and how shellcode operates at a low level. You can simulate a simple buffer overflow and watch the return address get overwritten—without risking your actual machine.
-
Educators Use the simulator as a teaching aid to explain CPU internals in a visual and interactive manner. I’ve seen professors struggle to explain the fetch-decode-execute cycle on a whiteboard. This tool makes it tangible.
Challenges I Faced
Building this wasn’t all smooth sailing. Let me share some of the trickier parts.
Memory addressing was tricky. Simulating a flat memory model in JavaScript/TypeScript sounds simple, but handling byte-level access, endianness, and alignment correctly required careful thought. x64 is little-endian, which means the least significant byte is stored at the lowest address. Getting this right in the memory viewer was important for accuracy.
Instruction parsing edge cases. Assembly syntax has a lot of quirks. Consider MOV QWORD PTR [RSP+8], RAX—that’s a valid instruction with a size specifier, a memory reference with an offset, and a register operand. Handling all these variations required a robust tokenizer and parser.
Keeping the UI responsive. When the user steps through instructions rapidly, the UI needs to update smoothly without lag. This meant being smart about re-renders and only updating the components that actually changed.
Future Enhancements
I’m actively working on improving this project. Some upcoming features include:
- Support for more instructions —
CALL,RET, conditional jumps likeJE,JNE,JG,JL - Flags register visualization — See the Zero Flag, Carry Flag, Sign Flag update in real time
- Assembly editor with syntax highlighting — Making the coding experience smoother
- Breakpoints and watch variables — Debug like a pro
- Step backward — Undo an instruction to see the previous state
- Improved memory inspection tools with different view modes (byte, word, dword, qword)
- Integration with tutorials for learning assembly programming from scratch
Open Source
Yes—this project is open source! I believe in the power of community collaboration. If you’re interested in contributing, testing, or even just playing around with it, you’re welcome to check it out on GitHub.
The codebase is written in TypeScript, so if you’re comfortable with that, you’ll feel right at home. Whether you want to add new instructions, improve the UI, fix bugs, or write documentation—contributions are welcome.
Final Thoughts
Building this CPU simulator has been one of the most rewarding projects I’ve undertaken. Not only has it deepened my own understanding of low-level computing, but it has also helped others visualize and grasp the fundamentals of CPU behavior.
There’s something deeply satisfying about watching a MOV instruction update a register in real time, or seeing the stack grow as you push values onto it. It makes the abstract feel real. And that’s exactly what I wanted to achieve with this project.
If you’re a student, educator, or just a curious hacker—give it a try. And if you have feedback or want to collaborate, feel free to reach out!
Thanks for reading!