← Back to Articles

How Software Virtualization Works

Featured Image

Have you ever wondered how it is possible to run a whole operating system inside another operating system? Programs like VirtualBox and QEMU allow you to run a full OS as if it were just another program. How is this possible? The answer is software virtualization. There are two types of virtualization: hardware virtualization and software virtualization (also known as emulation). This article will focus on software virtualization.

It’s Just a Program

The key to understanding how virtualization works is to realize that everything is just a program, or more specifically, a set of instructions that the CPU executes. When you run a program, the CPU reads the instructions and executes them one by one. There is no magic involved. When you run a virtual machine, like an operating system in VirtualBox, you are essentially feeding it a file (usually an ISO image) that contains the instructions to be executed. The operating system is just another program that the CPU executes.

Note: VirtualBox usually uses hardware virtualization, but it can also use software virtualization (emulation) if your CPU does not support hardware virtualization.

That means that we can create a program that will read the instructions one by one and execute them. If the instruction is MOV AX, 5 (put the number 5 in the AX register), we can create a variable for the AX register and assign it the value 5. If the instruction is to read or write something from RAM, we can create an array that represents the RAM and use it to read or write data. If the instruction is to draw a red dot on the screen, we can use JavaScript to draw a red dot on a element. This is the basic idea behind software virtualization.

v86 & LC-3

This means that we can use any programming language to virtualize/emulate software. One popular example is v86, a JavaScript-based x86 emulator. v86 is a full x86 emulator that can run Linux, DOS, Windows, and other x86-based operating systems in the browser using JavaScript. It is written in JavaScript and uses the HTML5 element to draw the screen. v86 is a great example of how software virtualization works.

Another example is the LC-3 (Little Computer 3) emulator. LC-3 is a simple computer architecture used in computer science courses to teach the basics of computer architecture. The LC-3 emulator is a simple program that reads LC-3 assembly instructions and executes them. It is written in C and can run on any platform that supports C. It’s great for being able to see exactly how a CPU executes instructions. There is even a TypeScript VM available that can run LC-3 programs in the browser – https://github.com/rumkin/lc3vm

Simple JavaScript Example Emulator

// Simple x86 emulator in JavaScript
// This emulator will load an executable (a series of instructions) and execute them one by one

// Registers
let AX = 0;
let BX = 0;
let CX = 0;
let DX = 0;

// Memory: Simulate 1024 bytes of memory
let memory = new Array(1024).fill(0);

// Example executable file (array of instructions)
// Each object represents an instruction with an opcode and operands
const executable = [
    { opcode: "MOV", operands: ["AX", 5] },   // Move 5 into AX
    { opcode: "ADD", operands: ["AX", 3] },   // Add 3 to AX
    { opcode: "MOV", operands: ["BX", 10] },  // Move 10 into BX
    { opcode: "ADD", operands: ["AX", "BX"] } // Add BX to AX
];

// Function to load the executable file into "memory"
// Here, we're loading the instructions into the first part of memory (from address 0)
function loadExecutable(executable) {
    for (let i = 0; i < executable.length; i++) {
        memory[i] = executable[i];
    }
}

// Function to fetch the next instruction from memory
// The 'instructionPointer' points to the current memory address
function fetchInstruction(instructionPointer) {
    return memory[instructionPointer];
}

// Function to execute an instruction
function executeInstruction(instruction) {
    let opcode = instruction.opcode;
    let operands = instruction.operands;

    if (opcode === "MOV") {
        let reg = operands[0];
        let val = operands[1];

        if (typeof val === "number") {
            window[reg] = val;
        } else {
            window[reg] = window[val];
        }
    } else if (opcode === "ADD") {
        let reg = operands[0];
        let val = operands[1];

        if (typeof val === "number") {
            window[reg] += val;
        } else {
            window[reg] += window[val];
        }
    }
}

// Initialize program execution
let instructionPointer = 0; // Start at memory address 0
loadExecutable(executable);  // Load the executable into memory

// Run the program: fetch and execute instructions
while (memory[instructionPointer] !== undefined) {
    let instruction = fetchInstruction(instructionPointer); // Fetch the current instruction
    executeInstruction(instruction);                         // Execute the fetched instruction
    instructionPointer++;                                    // Move to the next instruction
}

// Print the final state of the registers
console.log("AX:", AX);  // Output: AX: 18
console.log("BX:", BX);  // Output: BX: 10

Listen to Deep Dive

Download