How I built a web based CPU Simulator
Thilan Dissanayaka Pet Projects May 07, 2020

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.

  1. Fetch — The CPU reads the next instruction from memory, pointed to by the Instruction Pointer (RIP in x64).
  2. Decode — It figures out what the instruction means. Is it a MOV? An ADD? A JMP? What are the operands?
  3. 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:

  1. RSP is decremented by 8 (since we’re in 64-bit mode)
  2. The value of RAX is written to the memory address pointed to by the new RSP

And POP RBX does the reverse:

  1. The value at the memory address pointed to by RSP is read into RBX
  2. RSP is 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:

  1. Input — User types MOV RAX, 0x10 in the editor
  2. Tokenization — The raw string is split into tokens: ["MOV", "RAX", ",", "0x10"]
  3. Parsing — Tokens are analyzed to create an Instruction object with the opcode and typed operands
  4. Validation — The instruction is checked for valid operand combinations (you can’t MOV an immediate into an immediate, for example)
  5. Execution — The instruction handler for MOV is called, which resolves the source operand (0x10 = 16) and writes it to the destination (RAX)
  6. State Update — The register display updates to show RAX = 0x0000000000000010, and RIP advances to the next instruction
  7. 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 instructionsCALL, RET, conditional jumps like JE, 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.

https://github.com/thil4n/cpu-simulator

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!

ALSO READ
Blockchain 0x000 – Understanding the Fundamentals
May 21, 2020 Web3 Development

Imagine a world where strangers can exchange money, share data, or execute agreements without ever needing to trust a central authority. No banks, no intermediaries, no single point of failure yet...

Identity and Access Management (IAM)
May 11, 2020 Identity & Access Management

Who are you — and what are you allowed to do? That's the fundamental question every secure system must answer. And it's exactly what Identity and Access Management (IAM) is built to solve.

How I built a web based CPU Simulator
May 07, 2020 Pet Projects

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. This curiosity led me to...

Writing a Shell Code for Linux
Apr 21, 2020 Exploit Development

Shellcode is a small piece of machine code used as the payload in exploit development. In this post, we write Linux shellcode from scratch — starting with a simple exit, building up to spawning a shell, and explaining every decision along the way.

Exploiting a Stack Buffer Overflow on Windows
Apr 12, 2020 Exploit Development

In a previous tutorial we discusses how we can exploit a buffer overflow vulnerability on a Linux machine. I wen through all theories in depth and explained each step. Now today we are going to jump...

Access Control Models
Apr 08, 2020 Identity & Access Management

Access control is one of the most fundamental concepts in security. Every time you set file permissions, assign user roles, or restrict access to a resource, you're implementing some form of access control. But not all access control is created equal...

Exploiting a  Stack Buffer Overflow  on Linux
Apr 01, 2020 Exploit Development

Have you ever wondered how attackers gain control over remote servers? How do they just run some exploit and compromise a computer? If we dive into the actual context, there is no magic happening....

Basic concepts of Cryptography
Mar 01, 2020 Cryptography

Ever notice that little padlock icon in your browser's address bar? That's cryptography working silently in the background, protecting everything you do online. Whether you're sending an email,...

Common Web Application Attacks
Feb 05, 2020 Application Security

Web applications are one of the most targeted surfaces by attackers. This is primarily because they are accessible over the internet, making them exposed and potentially vulnerable. Since these...

Remote Code Execution (RCE)
Jan 02, 2020 Application Security

Remote Code Execution (RCE) is the holy grail of application security vulnerabilities. It allows an attacker to execute arbitrary code on a remote server — and the consequences are as bad as it sounds. In this post, we'll go deep into RCE across multiple languages, including PHP, Java, Python, and Node.js.