CPU Interaction & Execution Model

Descriptor tables, privilege rings, paging, interrupts, system calls, and user-mode stubs

Overview

guideXOS directly programs x86-64 CPU structures: GDT, IDT, TSS, paging (PML4), control registers (CR3/CR2), and APIC/LAPIC for interrupts and timers. Most of the low-level setup occurs in EntryPoint and subsequent kernel initialization calls.


Privilege Rings

The kernel configures both Ring 0 (kernel) and Ring 3 (user) code/data segments in the GDT. Current builds still execute application logic in kernel context; user-mode transition is stubbed. Planned progression:

Terminology: Ring 3 and "userspace" (or "user mode") refer to the same concept. Ring 3 is the CPU privilege level terminology from the x86 protection ring model, while userspace is the operating system terminology. Normal applications like Notepad run in Ring 3/userspace. The OS kernel runs in Ring 0 (kernel mode).
  • Ring 0 (Kernel Mode): All current code paths (drivers, GUI, loader, network stack) - unrestricted access to hardware and memory
  • Ring 3 (User Mode / Userspace): Defined selectors (UserCodeSelector/UserDataSelector), not yet entered - restricted privileges for security and stability
  • RPL: GDT sets user segment descriptors with DPL=3 for future isolation

Why Applications Run in Ring 3/Userspace:

  • Security: Prevents applications from directly accessing hardware or other processes' memory
  • Stability: Application crashes won't bring down the entire system
  • Isolation: The OS kernel mediates all resource access via system calls
Status: Foundation for ring-3 exists (selectors, TSS stack pointer) but an actual iretq transition is currently stubbed.

Global Descriptor Table (GDT) & TSS

Implementation: Kernel/Misc/GDT.cs. Descriptors created for kernel/user code & data plus a 64-bit TSS entry. The TSS holds privilege stack (RSP0) used when CPU transitions from Ring 3 -> Ring 0 on interrupts.

KernelCodeSelector = 0x08
KernelDataSelector = 0x10
UserCodeSelector   = 0x1B (0x18 | 3)
UserDataSelector   = 0x23 (0x20 | 3)
TssSelector        = 0x28
                    

The TSS is populated and IO bitmap disabled for user mode (future). SetKernelStack() updates RSP0 before user transitions.


Interrupt Descriptor Table (IDT) & Flow

Implementation: Kernel/Misc/IDT.cs. 256 entries are prepared, native assembly sets handler addresses, then Native.Load_IDT() loads IDTR. Interrupt handler export intr_handler processes:

  1. CPU exception decode (vectors < 0x20)
  2. System call (vector 0x80) dispatch via API.HandleSystemCall()
  3. Timer tick (0x20) -> scheduler + ThreadPool.Schedule()
  4. Driver-specific IRQ forwarding (Interrupts.HandleInterrupt())
  5. EOI signaling via PIC/APIC (Interrupts.EndOfInterrupt())

User software interrupts can be enabled per vector using IDT.AllowUserSoftwareInterrupt() (sets gate DPL=3).


Paging Model

Implementation: Kernel/Misc/PageTable.cs. Identity maps 0x1000 .. 4 GiB (4KiB pages). Functions:

  • Initialise() sets CR3 to shared PML4
  • Map()/MapUser() assign PTEs with present|rw|(user)
  • GetPageUser() path prepares user-accessible entries (sets U bit)

All allocations are physical + identity mapped; future address space isolation will introduce per-process PML4 cloning (see AddressSpace scaffold).


System Calls (int 0x80)

System calls are initiated via software interrupt 0x80 from user-mode applications. The current implementation uses a new syscall handler in IDT.cs that extracts arguments from CPU registers and calls Syscall.Handle().

intr_handler()
  if irq == 0x80:
     type      = rax (syscall type)
     arg1      = rdi
     arg2      = rsi
     arg3      = rdx
     processId = r12
     RAX       = Syscall.Handle(type, arg1, arg2, arg3, processId)
                    

Call convention: Arguments in rax, rdi, rsi, rdx. Process ID is passed in r12 (set during iretq transition). Return value in rax.


C++ Syscall Library (syscall.h)

To simplify system calls from native C++ applications, a header file named syscall.h is provided. This file should be included in your C++ projects. It provides inline functions that wrap the necessary assembly instructions to trigger a system call.

The library handles setting up the correct registers and executing the int 0x80 instruction. The kernel then uses the process ID (passed via the r12 register during the iretq transition) to associate the call with the correct process.

Available System Calls

  • syscall_exit() - Terminates the current process
  • syscall_console_write(const char* text) - Writes a null-terminated string to the console
  • syscall_console_read_line(char* buffer, uint64_t buffer_size) - Reads a line of text from the console

Header File (syscall.h)

#pragma once
#include <stdint.h>

// This enum must match the one in guideXOS/OS/Process.cs
enum SyscallType {
    Exit = 0,
    ConsoleWrite = 1,
    ConsoleReadLine = 2,
};

// Syscall to terminate the current process.
static inline void syscall_exit() {
    uint64_t type = SyscallType::Exit;
    asm volatile (
        "int $0x80"
        : 
        : "a"(type) // rax
        : "memory"
    );
}

// Syscall to write a null-terminated string to the console.
static inline void syscall_console_write(const char* text) {
    uint64_t type = SyscallType::ConsoleWrite;
    uint64_t text_ptr = (uint64_t)text;
    
    uint64_t len = 0;
    while (text[len]) {
        len++;
    }

    asm volatile (
        "int $0x80"
        : 
        : "a"(type), "D"(text_ptr), "S"(len) // rax, rdi, rsi
        : "memory"
    );
}

// Syscall to read a line of text from the console.
// Returns the number of characters read.
static inline uint64_t syscall_console_read_line(char* buffer, uint64_t buffer_size) {
    uint64_t type = SyscallType::ConsoleReadLine;
    uint64_t buffer_ptr = (uint64_t)buffer;
    uint64_t result;

    asm volatile (
        "int $0x80"
        : "=a"(result) // rax
        : "a"(type), "D"(buffer_ptr), "S"(buffer_size) // rax, rdi, rsi
        : "memory"
    );
    return result;
}

Example Usage

Here's a simple "Hello, World!" application that demonstrates interactive console I/O:

#include "syscall.h"

void _start() {
    syscall_console_write("Hello from a native GXM application!\\n");
    
    char buffer[128];
    syscall_console_write("Please enter your name: ");
    uint64_t bytesRead = syscall_console_read_line(buffer, 128);
    
    syscall_console_write("Hello, ");
    syscall_console_write(buffer);
    syscall_console_write("!\\n");

    syscall_exit();
}
Note: To compile this code into a GXM executable, you'll need to use a cross-compiler targeting x86-64 bare metal, then package the binary with the GXM header. The entry point must be named _start.

User Mode Stub & Planned Transition

File: UserModeStub.cs exports iret_to_user() as a placeholder. Real implementation will set up stack frame and execute iretq with:

RIP = entry
CS  = UserCodeSelector | RPL=3
RFLAGS = IF=1
RSP = user stack top
SS  = UserDataSelector | RPL=3
                    

Invocation: SchedulerExtensions.EnterUserMode(rip, rsp).


Executable Loading (GXM)

File: GXMLoader.cs. Steps:

  1. Validate header magic (GXM\0 or MUE\0)
  2. Allocate aligned region for image
  3. Identity map pages with MapUser() (preparing for future ring-3)
  4. Allocate & map user stack (64 KiB)
  5. Call SchedulerExtensions.EnterUserMode() (currently logs stub)

Supports GUI script preface for dynamic window creation (no native execution).


Multi-core (SMP)

File: SMP.cs. Bootstrap CPU (ID=0) initializes global structures, copies trampoline, sends INIT + STARTUP IPIs to APs. Each AP:

  • Enables SSE
  • Starts Local APIC timer
  • Initializes ThreadPool (shared structures)
  • Enters idle halt loop

Shared structures: GDT, IDT, PageTable root at fixed physical addresses in low memory block.


Roadmap / Next Steps

  • Implement real iret_to_user assembly stub and TSS ltr
  • Per-process address space: copy kernel PML4 slots only
  • System call ABI formalization (register usage, error codes)
  • User-land exception handling and signal model
  • Scheduler: separate kernel vs user threads
  • Security: validate user pointers, restrict IO port access
Summary:

All foundational CPU structures are in place for a protected ring-3 environment. Transition mechanics and isolation remain to be implemented; current execution still runs in supervisor context.


Related Topics