Files
nt-lite/KERNEL_DESIGN.md
T
2026-04-30 23:52:46 +02:00

38 KiB
Raw Blame History

Hybrid Kernel Architecture Design Specification


1. Overview & Philosophy

The kernel manages memory, schedules threads, routes interrupts, and brokers messages. No filesystem, no network stack, no GUI. Everything else lives in user-mode code or drivers loaded at boot.

Three governing rules:

  • Kernel is unreachable from ring 3. Supervisor pages, full stop.
  • User space is one flat shared heap. There is no per-process address space. All user code lives in the same virtual range and can reach any of it. No code signing certificates, no TPM, just the checksum already in the format.

Key influences:

  • Executive objects - typed, reference-counted kernel objects for everything the kernel tracks.
  • A HAL sitting between the kernel and hardware so drivers never touch ports directly.
  • A clean separation between the object manager, the scheduler, and the memory manager.

Key influences rejected:

  • UNIX naming (no pmm_alloc_frame, no ke_spin_lock, no snake_case in kernel code).
  • UTF-16 anywhere. The kernel is UTF-8/ASCII. char * throughout.
  • SYSCALL/SYSRET complexity. Interrupts are simpler and sufficient: INT 0x2E.
  • GCC-isms. This is MSVC.

2. Component Map

bootx64.efi
│
├── reads boot.ini (debug flag, future knobs)
├── reads drivers/drivers.ini
├── loads:
│       kernel.exe
│       hal.dll
│       syscall.dll
│       win32.dll
│       drivers/*.sys
│
├── converts EFI memory map -> MEMORY_MAP
└── jumps to kernel entry point in ring 0

                    ┌──────────────────────────────────┐
ring 0              │           kernel.exe             │
(upper half)        │   Mm │ Ke │ Ob │ Io │ scheduler  │
                    ├──────────────────────────────────┤
                    │             hal.dll              │
                    │   APIC │ HPET │ GOP │ PS/2       │
                    │   PCI  │ disk dispatch           │
                    │   DbgPrint                       │
                    └──────────────────────────────────┘
                    ┌──────────────────────────────────┐
ring 0 (loadable)   │    ide.sys        ahci.sys       │
                    └──────────────────────────────────┘
                    ┌──────────────────────────────────┐
ring 3              │            win32.dll             │
(lower half)        │   kernel32 │ user32 │ ole32      │
                    ├──────────────────────────────────┤
                    │            syscall.dll           │
                    │   INT 0x2E stubs                │
                    └──────────────────────────────────┘

3. Naming Convention

All kernel-visible identifiers follow PrefixActionObject:

<Subsystem><Verb><Noun>

Subsystem prefixes

Prefix Subsystem
Mm Memory manager
Ke Kernel executive (sched, sync)
Ob Object manager
Io I/O manager / driver interface
Hal Hardware abstraction layer
Ldr Loader (PE image loading)
Dbg Debug

Examples

MmAllocatePool(size)
MmFreePool(ptr)
MmMapPage(virt, phys, flags)

KeAcquireMutex(mutex)
KeReleaseMutex(mutex)
KeSignalEvent(event)
KeWaitForObject(object)
KeYield()

ObReferenceObject(object)
ObDereferenceObject(object)
ObAllocateObject(type, size)

IoRegisterDisk(index, driver)
IoReadSectors(drive, lba, count, buf)

HalQueryTicks()
HalStall(us)
HalInitialize(bootInfo)

LdrLoadImage(buffer, size, baseOut)
LdrResolveImport(dllName, functionName)

DbgPrint(Format, ...)

Types and structs

Structs are named NOUN in all-caps with no prefix. The prefix appears only on functions and global variables, not on type names:

typedef struct PROCESS   PROCESS;
typedef struct THREAD    THREAD;
typedef struct MUTEX     MUTEX;
typedef struct SEMAPHORE SEMAPHORE;
typedef struct EVENT     EVENT;
typedef struct QUEUE     QUEUE;
typedef struct OBJECT    OBJECT;

Return type

All fallible kernel functions return KSTATUS. Infallible helpers (queries, ref bumps) may return their value directly.


4. Memory Architecture

4.1 Virtual Address Space

0x0000000000000000  ┌──────────────────────────────────┐
                    │                                  │
                    │   User Space  (lower half)       │
                    │                                  │
                    │   One flat shared heap.          │
                    │   All processes and all their    │
                    │   allocations live here.         │
                    │   Ring 3 readable and writable.  │
                    │                                  │
0x00007FFFFFFFFFFF  └──────────────────────────────────┘
         ...             (non-canonical hole)
0xFFFF800000000000  ┌──────────────────────────────────┐
                    │   kernel.exe   [+0 MB .. +128 MB]│
                    ├──────────────────────────────────┤
                    │   hal.dll, syscall.dll,          │
                    │   win32.dll, *.sys               │
                    │   (packed above kernel.exe)      │
                    ├──────────────────────────────────┤
                    │   Direct physical window         │
                    │   0xFFFF880000000000 +           │
                    ├──────────────────────────────────┤
                    │   Kernel pool (kmalloc)          │
                    │   0xFFFFC00000000000 +           │
                    └──────────────────────────────────┘

Invariant: Every PML4 entry covering the upper half has U/S = 0. A ring-3 access to any upper-half address is always a hard fault. No fixups, no guard pages, no recovery.

There is one PML4 shared by all processes. The lower-half entries point to the shared user heap pages. The upper-half entries are identical in every CR3.

4.2 Custom Memory Map

The bootloader converts the EFI memory map into a MEMORY_MAP before calling ExitBootServices. The kernel never sees EFI types.

// memory_map.h  - shared between bootx64.efi and kernel.exe

#pragma once
#include <stdint.h>

#define MEMORY_MAP_MAGIC    0x4D454D4D41500000ULL   /* 'MEMMAP\0\0' */
#define MEMORY_MAX_REGIONS  256

typedef enum MEMORY_TYPE {
    MemoryUsable          = 0,   /* free RAM, available to the PMM      */
    MemoryReserved        = 1,   /* firmware-reserved, do not touch     */
    MemoryMappedIO        = 2,   /* device MMIO regions                 */
    MemoryFramebuffer     = 3,   /* GOP framebuffer                     */
    MemoryBootloader      = 4,   /* bootloader code/data (reclaimable)  */
    MemoryKernel          = 5,   /* kernel + HAL + drivers image pages  */
} MEMORY_TYPE;

typedef struct MEMORY_REGION {
    uint64_t    Base;
    uint64_t    Length;          /* in bytes, always page-aligned       */
    MEMORY_TYPE Type;
    uint32_t    _pad;
} MEMORY_REGION;

typedef struct MEMORY_MAP {
    uint64_t       Magic;
    uint32_t       RegionCount;
    uint32_t       _pad;
    MEMORY_REGION  Regions[MEMORY_MAX_REGIONS];
} MEMORY_MAP;

4.3 Physical Memory Manager

Stack allocator over 4 KB frames, seeded from all MemoryUsable regions in MEMORY_MAP.

// mm_phys.h

#pragma once
#include <stdint.h>
#include <stddef.h>

#define PAGE_SIZE   4096ULL

typedef uint64_t PHYSICAL_ADDRESS;

void             MmInitializePhysical(const MEMORY_MAP* map);
PHYSICAL_ADDRESS MmAllocateFrame(void);
PHYSICAL_ADDRESS MmAllocateContiguous(size_t count);
void             MmFreeFrame(PHYSICAL_ADDRESS frame);
size_t           MmFreeFrameCount(void);
size_t           MmTotalFrameCount(void);

Internals: a stack of free PHYSICAL_ADDRESS values in a region carved out before any allocation is served. Lock is a spinlock; the stack is trivially O(1) for both push and pop.

4.4 Virtual Memory Manager

// mm_virt.h

#pragma once
#include <stdint.h>
#include <stddef.h>

/* Page flags */
#define PAGE_PRESENT   (1ULL << 0)
#define PAGE_WRITE     (1ULL << 1)
#define PAGE_USER      (1ULL << 2)
#define PAGE_NOCACHE   (1ULL << 4)
#define PAGE_NX        (1ULL << 63)

KSTATUS MmMapPage(uintptr_t virt, PHYSICAL_ADDRESS phys, uint64_t flags);
KSTATUS MmUnmapPage(uintptr_t virt);

/* Kernel pool - ring 0 only, never PAGE_USER. */
void* MmAllocatePool(size_t size);
void    MmFreePool(void* ptr);

/* User heap - shared flat space, ring 3 accessible. */
void* MmAllocateUserMemory(size_t size);
void    MmFreeUserMemory(void* ptr, size_t size);

/* Load the shared PML4 into CR3. Called once at boot. */
void    MmActivate(void);

Kernel pool uses a best-fit free list allocator.

User memory is a single global best-fit free list allocator backed by the PMM. All user allocations are mapped PAGE_PRESENT | PAGE_WRITE | PAGE_USER. There are no per-process heaps, no virtual address descriptors, no separate regions per process - one heap, everyone shares it.


5. Boot Sequence - bootx64.efi

5.1 boot.ini

Sits at the root of the ESP. Read by the bootloader before anything else.

[kernel]
debug = 1

The debug value is passed through BOOT_INFO to the kernel, which hands it to HalInitialize. The HAL sets an internal HalDebugEnabled flag from it. No other mechanism controls DbgPrint output at this time.

Future knobs (serial port, memory cap, etc.) belong here, not in drivers.ini.

5.2 Responsibilities

  1. Parse boot.ini.
  2. Read drivers/drivers.ini to build the driver load list.
  3. Load each PE image (kernel.exe, hal.dll, syscall.dll, win32.dll, all listed .sys files) from the ESP using UEFI file protocol.
  4. Validate each image: the PE32+ loader computes the PE checksum and compares it to IMAGE_OPTIONAL_HEADER64.CheckSum. Mismatch → print error via ConOut → halt.
  5. Map kernel images into the upper half of a freshly constructed PML4. Map all physical RAM as a direct window at 0xFFFF880000000000.
  6. Convert the EFI memory map to MEMORY_MAP.
  7. Call ExitBootServices.
  8. Set up a minimal GDT (ring 0 code/data, ring 3 code/data), a minimal IDT (fault stubs + INT 0x2E handler vector), enable NXE in EFER, set CR3.
  9. Fill BOOT_INFO and jump to kernel.exe entry point.

5.3 PE Checksum Validation

The checksum algorithm is the standard PE checksum (16-bit word accumulation with carry folding, then the file size is added). The bootloader rejects any image whose computed checksum does not match the value stored in the optional header. This is enforced on every image, every boot. There is no override, no skip flag.

5.4 Boot Information Block

// boot_info.h  - shared between bootx64.efi and kernel.exe

#pragma once
#include <stdint.h>
#include "memory_map.h"

#define BOOT_INFO_MAGIC    0x424F4F54494E464FULL   /* 'BOOTINFO' */
#define MAX_BOOT_MODULES   32

typedef struct BOOT_MODULE {
    char      Name[32];        /* e.g. "hal.dll", "ide.sys"   */
    uintptr_t VirtBase;        /* where it was mapped          */
    size_t    ImageSize;
} BOOT_MODULE;

typedef struct BOOT_INFO {
    uint64_t      Magic;

    /* Physical memory (our format, not EFI's). */
    MEMORY_MAP    MemoryMap;

    /* GOP framebuffer. */
    uintptr_t     FramebufferBase;
    uint32_t      FramebufferWidth;
    uint32_t      FramebufferHeight;
    uint32_t      FramebufferStride;   /* pixels per row */

    /* Loaded modules. */
    BOOT_MODULE   Modules[MAX_BOOT_MODULES];
    uint32_t      ModuleCount;

    /* ACPI RSDP physical address (for APIC, HPET). */
    uintptr_t     RsdpPhysical;

    /* Direct physical window base. */
    uintptr_t     PhysicalMapBase;     /* = 0xFFFF880000000000 */

    /* From boot.ini */
    uint32_t      DebugEnabled;
} BOOT_INFO;

6. Kernel - kernel.exe

6.1 Status Codes - KSTATUS

All fallible kernel functions return KSTATUS. Success is always zero. Failures are positive named constants. No errno negatives, no HRESULT sign games.

// ke_status.h

#pragma once
#include <stdint.h>

typedef uint32_t KSTATUS;

#define STATUS_SUCCESS              ((KSTATUS)0x00000000)
#define STATUS_FAILURE              ((KSTATUS)0x00000001)
#define STATUS_INVALID_PARAMETER    ((KSTATUS)0x00000002)
#define STATUS_NO_MEMORY            ((KSTATUS)0x00000003)
#define STATUS_NOT_FOUND            ((KSTATUS)0x00000004)
#define STATUS_ACCESS_DENIED        ((KSTATUS)0x00000005)
#define STATUS_ALREADY_EXISTS       ((KSTATUS)0x00000006)
#define STATUS_NOT_SUPPORTED        ((KSTATUS)0x00000007)
#define STATUS_TIMEOUT              ((KSTATUS)0x00000008)
#define STATUS_BUFFER_TOO_SMALL     ((KSTATUS)0x00000009)
#define STATUS_DEVICE_ERROR         ((KSTATUS)0x0000000A)
#define STATUS_OBJECT_DEAD          ((KSTATUS)0x0000000B)
#define STATUS_QUEUE_FULL           ((KSTATUS)0x0000000C)
#define STATUS_QUEUE_EMPTY          ((KSTATUS)0x0000000D)
#define STATUS_CHECKSUM_MISMATCH    ((KSTATUS)0x0000000E)

#define KSUCCESS(s)  ((s) == STATUS_SUCCESS)

6.2 Object Model

Every resource the kernel tracks is an executive object: a typed, reference-counted blob allocated from the kernel pool. The object header is always the first member of any object struct - this allows the object manager to operate on any object through a pointer to OBJECT.

// ob_object.h

#pragma once
#include <stdint.h>
#include "ke_status.h"

typedef enum OBJECT_TYPE {
    ObjectTypeProcess   = 1,
    ObjectTypeThread    = 2,
    ObjectTypeMutex     = 3,
    ObjectTypeSemaphore = 4,
    ObjectTypeEvent     = 5,
    ObjectTypeQueue     = 6,
} OBJECT_TYPE;

typedef struct OBJECT {
    OBJECT_TYPE   Type;
    volatile long RefCount;           /* interlocked */
    void        (*Destroy)(struct OBJECT* self);
} OBJECT;

/* Increment the reference count. Call whenever ownership is shared. */
void ObReferenceObject(OBJECT* object);

/* Decrement. Calls Destroy() and frees the object when count reaches zero. */
void ObDereferenceObject(OBJECT* object);

/* Allocate a zeroed object of the given size from kernel pool.
   Caller fills Type and Destroy before publishing the pointer. */
OBJECT* ObAllocateObject(OBJECT_TYPE type, size_t size);

6.3 Process Model

A process owns a handle table and a list of threads. It does not own an address space - there is one shared address space and it belongs to everyone.

// ke_process.h

#pragma once
#include "ob_object.h"
#include "ke_list.h"
#include "ke_spinlock.h"

#define PROCESS_NAME_MAX    64
#define HANDLE_TABLE_SIZE   256

typedef struct PROCESS {
    OBJECT       Header;            /* must be first */

    uint32_t     ProcessId;
    char         Name[PROCESS_NAME_MAX];   /* UTF-8 */

    /* Thread list. */
    LIST_ENTRY   ThreadList;        /* links THREAD.ProcessLink */
    uint32_t     ThreadCount;

    /* Handle table.
       Index 0 is always NULL (the invalid handle).
       Entries are OBJECT pointers, each holding a reference. */
    OBJECT* Handles[HANDLE_TABLE_SIZE];
    SPINLOCK     HandleLock;

    int          ExitCode;
    LIST_ENTRY   GlobalLink;        /* node in the global process list */
} PROCESS;

KSTATUS  KeCreateProcess(const char* name, PROCESS **out);
void     KeExitProcess(PROCESS* process, int exitCode);
KSTATUS  KeAllocateHandle(PROCESS* process, OBJECT* object, uint32_t* handleOut);
void     KeCloseHandle(PROCESS* process, uint32_t handle);
OBJECT* KeLookupHandle(PROCESS* process, uint32_t handle);

6.4 Thread Model

A thread is the unit of scheduling. Every thread belongs to exactly one process.

// ke_thread.h

#pragma once
#include "ob_object.h"
#include "ke_list.h"
#include <stdint.h>

#define KSTACK_SIZE   (64  * 1024)    /* 64 KB kernel stack  */
#define USTACK_SIZE   (512 * 1024)    /* 512 KB user stack   */

typedef enum THREAD_PRIORITY {
    PriorityLow    = 0,
    PriorityNormal = 1,
    PriorityHigh   = 2,
} THREAD_PRIORITY;

typedef enum THREAD_STATE {
    ThreadReady   = 0,
    ThreadRunning = 1,
    ThreadWaiting = 2,
    ThreadDead    = 3,
} THREAD_STATE;

/*
 * Saved register context - matches the layout pushed by KeSwitchContext
 * in ke_switch.asm. The saved RSP points to this structure on the
 * kernel stack; restoring RSP and popping is the entire switch.
 */
typedef struct KCONTEXT {
    uint64_t R15, R14, R13, R12;
    uint64_t Rbp, Rbx;
    uint64_t Rip;      /* return address within KeSwitchContext */
} KCONTEXT;

typedef struct THREAD {
    OBJECT          Header;

    uint32_t        ThreadId;
    THREAD_STATE    State;
    THREAD_PRIORITY Priority;

    /* Scheduler feedback: quanta consumed without blocking.
       Used by the MLFQ to decide demotion. */
    uint32_t        QuantaConsumed;

    struct PROCESS* Owner;
    LIST_ENTRY      ProcessLink;   /* node in Owner->ThreadList  */
    LIST_ENTRY      SchedulerLink; /* node in run queue or wait list */

    /* Kernel stack - never PAGE_USER. */
    void* KernelStackBase;
    uintptr_t       KernelStackTop;

    /* Saved context pointer (set by KeSwitchContext). */
    KCONTEXT* SavedContext;

    /* Non-NULL when State == ThreadWaiting. */
    OBJECT* WaitObject;

    /* TSS RSP0 value for this thread, updated on every context switch. */
    uintptr_t       TssRsp0;

    /* Ring-3 entry point and argument for new threads. */
    uintptr_t       UserEntry;
    uintptr_t       UserArg;
} THREAD;

KSTATUS KeCreateThread(PROCESS* owner, uintptr_t entry, uintptr_t arg,
                       THREAD_PRIORITY priority, THREAD **out);
void    KeExitThread(THREAD* thread, int code);

Kernel vs. user stacks: The kernel stack is allocated from kernel pool, mapped without PAGE_USER, and pointed to by TssRsp0. On INT 0x2E or any hardware interrupt the CPU automatically loads this address from the TSS, so ring-3 code never touches the kernel stack. The user stack lives in the shared user heap, allocated by MmAllocateUserMemory when the thread is created.

6.5 Scheduler - Multi-Level Feedback Queue

Three run queues, one per priority level. A thread starts at PriorityNormal unless specified otherwise. The scheduler moves threads between levels based on behaviour:

  • Demotion: a thread that exhausts its full quantum without blocking is moved down one level (floor: PriorityLow).
  • Promotion: a thread that blocks voluntarily before its quantum expires is moved up one level (ceiling: PriorityHigh).
  • Dispatch: always pick from the highest non-empty queue. Within a queue, round-robin.

This rewards I/O-bound and interactive threads (which block often) with higher priority, and keeps CPU-bound threads from starving other work without dropping them entirely.

// ke_sched.h

#pragma once
#include "ke_thread.h"

#define QUANTUM_MS  10   /* base time slice in milliseconds */

void    KeInitializeScheduler(void);
void    KeReadyThread(THREAD* thread);
void    KeUnreadyThread(THREAD* thread);
void    KeSchedulerTick(void);      /* called from timer ISR every QUANTUM_MS */
void    KeYield(void);
THREAD* KeCurrentThread(void);

KeSchedulerTick logic:

  1. Increment CurrentThread->QuantaConsumed.
  2. If the thread has blocked since last tick, promote it (already off the run queue - apply the priority change before it is re-readied).
  3. If QuantaConsumed exceeds the threshold for the current priority level, demote and reset.
  4. Find the highest non-empty queue. If it is higher priority than the current thread, preempt immediately; otherwise let the current thread finish its quantum.
  5. Call KeSwitchContext(current, next).

Context switch - ke_switch.asm (MASM64):

; void KeSwitchContext(THREAD* from, THREAD* to)
; RCX = from, RDX = to   (Microsoft x64 ABI)

KeSwitchContext PROC
    push    rbx
    push    rbp
    push    r12
    push    r13
    push    r14
    push    r15
    mov     [rcx + THREAD_SavedContext], rsp    ; from->SavedContext = rsp
    mov     rsp, [rdx + THREAD_SavedContext]    ; rsp = to->SavedContext
    ; Update TSS.RSP0 so the next interrupt uses the incoming thread's kernel stack
    mov     rax, [rdx + THREAD_TssRsp0]
    mov     qword ptr [tss + TSS_RSP0_OFFSET], rax
    pop     r15
    pop     r14
    pop     r13
    pop     r12
    pop     rbp
    pop     rbx
    ret
KeSwitchContext ENDP

THREAD_SavedContext, THREAD_TssRsp0, and TSS_RSP0_OFFSET are numeric offsets defined in a shared offsets.inc generated by the build system from the C struct definitions.

KeCurrentThread() reads from gs:[0], a per-CPU slot written by the bootstrap code. Single-core for now; SMP is a matter of setting up per-core GS bases.

6.6 Synchronization Primitives

Spinlock

Not an executive object. Used inside the kernel for short critical sections that must not sleep and may run in interrupt context. Never hold a spinlock across a scheduling boundary.

Implemented in ke_spinlock.h as an inline interlocked CAS loop using MSVC intrinsics (_InterlockedCompareExchange) with a _mm_pause() spin body. No assembly file needed.

// ke_spinlock.h

#pragma once
#include <stdint.h>
#include <intrin.h>

typedef struct SPINLOCK {
    volatile long Locked;
} SPINLOCK;

#define SPINLOCK_INIT  { 0 }

static __forceinline void KeAcquireSpinlock(SPINLOCK* sl) {
    while (_InterlockedCompareExchange(&sl->Locked, 1, 0) != 0)
        _mm_pause();
}

static __forceinline void KeReleaseSpinlock(SPINLOCK* sl) {
    _InterlockedExchange(&sl->Locked, 0);
}

Mutex

Binary, ownership-based. Only the acquiring thread may release it. Blocking threads sleep on a wait list; the scheduler wakes exactly one on release.

typedef struct MUTEX {
    OBJECT       Header;
    SPINLOCK     Lock;
    THREAD* Owner;         /* NULL if unowned */
    LIST_ENTRY   WaitList;
} MUTEX;

KSTATUS MutexCreate(MUTEX **out);
KSTATUS KeAcquireMutex(MUTEX* mutex);
KSTATUS KeTryAcquireMutex(MUTEX* mutex);
void    KeReleaseMutex(MUTEX* mutex);

Semaphore

Counter-based. KeWaitSemaphore decrements (blocks at zero); KeSignalSemaphore increments and wakes one waiter per call.

typedef struct SEMAPHORE {
    OBJECT       Header;
    SPINLOCK     Lock;
    int32_t      Count;
    int32_t      MaxCount;
    LIST_ENTRY   WaitList;
} SEMAPHORE;

KSTATUS SemaphoreCreate(int32_t initial, int32_t max, SEMAPHORE **out);
KSTATUS KeWaitSemaphore(SEMAPHORE* semaphore);
KSTATUS KeTrySemaphore(SEMAPHORE* semaphore);
void    KeSignalSemaphore(SEMAPHORE* semaphore);

Event

Two modes: auto-reset and manual-reset.

typedef enum EVENT_TYPE { EventAutoReset, EventManualReset } EVENT_TYPE;

typedef struct EVENT {
    OBJECT       Header;
    SPINLOCK     Lock;
    EVENT_TYPE   Type;
    uint32_t     Signaled;
    LIST_ENTRY   WaitList;
} EVENT;

KSTATUS KeEventCreate(EVENT_TYPE type, uint32_t initialState, EVENT **out);
void    KeSignalEvent(EVENT* event);
void    KeClearEvent(EVENT* event);
KSTATUS KeWaitForEvent(EVENT* event);

Common wait mechanism: KeBlockThread(thread, object) - marks the thread ThreadWaiting, records WaitObject, removes it from the run queue, and calls KeYield(). The waking side calls KeUnblockThread(thread) which clears WaitObject, promotes the thread, and calls KeReadyThread. This path is shared by all three blocking primitives.

6.7 IPC - Message Queues

Processes share memory, so queues are a synchronization and ordering mechanism, not a data-transfer one. A pointer in the payload is valid on both sides.

// ke_queue.h

#pragma once
#include "ob_object.h"
#include "ke_spinlock.h"
#include <stdint.h>
#include <stdbool.h>

#define QUEUE_MESSAGE_SIZE   64     /* bytes, fixed */
#define QUEUE_DEPTH          256

typedef struct QUEUE_MESSAGE {
    uint32_t Type;          /* caller-defined message class */
    uint32_t Flags;
    uint64_t Param[6];      /* 48 bytes of payload          */
} QUEUE_MESSAGE;            /* total: 64 bytes              */

typedef struct QUEUE {
    OBJECT          Header;
    SPINLOCK        Lock;
    QUEUE_MESSAGE   Ring[QUEUE_DEPTH];
    uint32_t        Head, Tail, Count;
    LIST_ENTRY      SendWaiters;
    LIST_ENTRY      RecvWaiters;
} QUEUE;

KSTATUS QueueCreate(QUEUE **out);
KSTATUS KeSendMessage(QUEUE* queue, const QUEUE_MESSAGE* msg, bool blocking);
KSTATUS KeReceiveMessage(QUEUE* queue, QUEUE_MESSAGE* msg, bool blocking);

7. Hardware Abstraction Layer - hal.dll

hal.dll is loaded into ring 0 alongside the kernel. The kernel retrieves its interface once at boot via LdrGetExport("hal.dll", "HalGetApi"). Drivers receive the same pointer through their DriverEntry call.

7.1 HAL API Table

// hal_api.h

#pragma once
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "ke_status.h"

/* Screen - GOP framebuffer */
typedef struct HAL_SCREEN {
    void     (*Clear)(uint32_t color);
    void     (*PutPixel)(int x, int y, uint32_t color);
    void     (*Blit)(int x, int y, int w, int h, const uint32_t* pixels);
    void     (*GetSize)(int* width, int* height);
    void     (*Scroll)(int lines);
    void     (*PutChar)(char c, int x, int y, uint32_t fg, uint32_t bg);
} HAL_SCREEN;

/* Keyboard - PS/2, interrupt-driven ring buffer */
typedef struct HAL_KEYBOARD {
    bool     (*Poll)(void);              /* true if a key is available */
    uint8_t  (*ReadScancode)(void);
    char     (*ReadChar)(void);          /* 0 if nothing pending       */
    void     (*SetCallback)(void (*cb)(uint8_t scancode));
} HAL_KEYBOARD;

/* Disk - abstract 512-byte sector interface */
typedef struct HAL_DISK {
    KSTATUS  (*ReadSectors)(uint8_t drive, uint64_t lba,
                            uint32_t count, void* buf);
    KSTATUS  (*WriteSectors)(uint8_t drive, uint64_t lba,
                             uint32_t count, const void* buf);
    bool     (*DrivePresent)(uint8_t drive);
    uint64_t (*DriveSectorCount)(uint8_t drive);
} HAL_DISK;

/* Timer - HPET preferred, APIC timer fallback */
typedef struct HAL_TIMER {
    uint64_t (*QueryTicks)(void);       /* milliseconds since boot */
    void     (*Stall)(uint32_t us);
} HAL_TIMER;

/* PCI - config space via port I/O */
typedef struct HAL_PCI {
    uint32_t (*ConfigRead32)(uint8_t bus, uint8_t dev,
                             uint8_t fn, uint8_t reg);
    void     (*ConfigWrite32)(uint8_t bus, uint8_t dev,
                              uint8_t fn, uint8_t reg, uint32_t val);
    bool     (*FindDevice)(uint16_t vendor, uint16_t device,
                           uint8_t* bus, uint8_t* dev, uint8_t* fn);
    bool     (*FindClass)(uint8_t classCode, uint8_t subclass,
                          uint8_t* bus, uint8_t* dev, uint8_t* fn);
} HAL_PCI;

/* Interrupt controller - local APIC + I/O APIC */
typedef struct HAL_INTC {
    void (*RegisterIrq)(uint8_t irq, void (*handler)(void));
    void (*MaskIrq)(uint8_t irq);
    void (*UnmaskIrq)(uint8_t irq);
    void (*SendEoi)(void);
} HAL_INTC;

typedef struct HAL_API {
    uint32_t     Version;          /* HAL_API_VERSION */
    HAL_SCREEN   Screen;
    HAL_KEYBOARD Keyboard;
    HAL_DISK     Disk;
    HAL_TIMER    Timer;
    HAL_PCI      Pci;
    HAL_INTC     Intc;
} HAL_API;

#define HAL_API_VERSION  1

/* Exported by hal.dll. */
const HAL_API* HalGetApi(void);
KSTATUS        HalInitialize(const BOOT_INFO* bootInfo);

7.2 DbgPrint

/* Declared in hal_api.h, implemented in hal.dll, ring 0 only.
   Not exported to ring 3. No KSTATUS - void, always. */

void DbgPrint(const char* fmt, ...);

DbgPrint is always compiled in. On entry it checks the internal HalDebugEnabled flag (set by HalInitialize from BOOT_INFO.DebugEnabled) and returns immediately if clear. When enabled, it formats the string and writes to the console. The implementation is not specified here - just noted as writing to console output.

Kernel code, HAL internals, and drivers all call DbgPrint freely. There is no ring-3 equivalent and no syscall wrapper for it.

7.3 HAL Internals Summary

Screen: Direct writes to the GOP framebuffer from BOOT_INFO. An embedded 8×16 bitmap font covers printable ASCII. DbgPrint uses this for output.

Keyboard: IRQ 1 via I/O APIC. PS/2 controller on ports 0x60/0x64. Scancodes collected into a 256-entry ring buffer; ReadChar translates set-2 scancodes to ASCII.

Disk: Dispatch table of up to 8 logical drives. Populated by drivers calling IoRegisterDisk during DriverEntry. HAL_DISK functions index into this table.

Timer: HPET comparator fires every 1 ms, increments a uint64_t tick counter. Falls back to the APIC timer calibrated against the PIT if no HPET is found.

PCI: Mechanism 1 port I/O (0xCF8/0xCFC). No PCIe extended config space for now.

APIC: Local APIC MMIO base discovered from the MADT. I/O APIC IRQ routing from MADT. IRQ 0 drives the scheduler tick. IRQ 1 is the keyboard.


8. System Call Interface - syscall.dll

8.1 Why INT 0x2E

SYSCALL/SYSRET requires programming MSR_STAR, MSR_LSTAR, MSR_SFMASK, and careful handling of the segment registers on return. INT 0x2E uses the IDT we already set up for hardware interrupts - one IDT gate, one handler, done. The performance difference does not matter at this scale.

8.2 IDT Entry and Kernel Handler

Vector 0x2E is installed as an interrupt gate (IF cleared on entry, ring-3 callable - DPL=3 in the gate descriptor).

The handler is a MASM64 stub:

; ke_syscall_entry in ke_syscall.asm
; On entry: RAX = syscall number, RCX/RDX/R8/R9/R10/R11 = args
; (Microsoft x64 ABI: first four args in RCX, RDX, R8, R9;
;  we repurpose R10 and R11 for args 5 and 6)

KeSyscallEntry PROC
    ; The CPU has already pushed SS, RSP, RFLAGS, CS, RIP
    ; and switched to the kernel stack via TSS.RSP0.
    sub     rsp, 8                      ; align to 16 bytes
    push    r11
    push    r10
    push    r9
    push    r8
    push    rdx
    push    rcx
    push    rax
    call    KeSyscallDispatch           ; C function, args already on stack
    add     rsp, 7 * 8                  ; clean args
    iretq
KeSyscallEntry ENDP

KeSyscallDispatch is a plain C function in ke_syscall.c that dispatches on nr through a function pointer table indexed by SYS_* constants.

8.3 Syscall Numbers

// syscall_nr.h  - shared between kernel and syscall.dll

#pragma once

#define SYS_EXIT_THREAD      0
#define SYS_EXIT_PROCESS     1
#define SYS_SPAWN            2
#define SYS_SLEEP            3
#define SYS_YIELD            4
#define SYS_ALLOC            5
#define SYS_FREE             6
#define SYS_CREATE_THREAD    7
#define SYS_OPEN_HANDLE      8
#define SYS_CLOSE_HANDLE     9
#define SYS_MUTEX_CREATE     10
#define SYS_MUTEX_ACQUIRE    11
#define SYS_MUTEX_RELEASE    12
#define SYS_SEM_CREATE       13
#define SYS_SEM_WAIT         14
#define SYS_SEM_SIGNAL       15
#define SYS_EVENT_CREATE     16
#define SYS_EVENT_SET        17
#define SYS_EVENT_WAIT       18
#define SYS_QUEUE_CREATE     19
#define SYS_QUEUE_SEND       20
#define SYS_QUEUE_RECV       21
#define SYS_SCREEN_BLIT      22
#define SYS_GET_TICK         23

#define SYS_MAX              24

8.4 User-Side Stubs - syscall.dll

Each syscall is a MASM64 procedure. syscall.dll exports them by name. User code links against syscall.dll and calls them as normal functions.

; syscall_stubs.asm
; Microsoft x64 ABI: args in RCX, RDX, R8, R9, then stack.
; We move arg 5 and 6 into R10 and R11 before INT.

SysYield PROC
    mov     rax, SYS_YIELD
    int     02Eh
    ret
SysYield ENDP

SysAlloc PROC
    ; RCX = size (already in place)
    mov     rax, SYS_ALLOC
    int     02Eh
    ret
SysAlloc ENDP

SysFree PROC
    ; RCX = ptr, RDX = size
    mov     rax, SYS_FREE
    int     02Eh
    ret
SysFree ENDP

; ... one PROC per SYS_* constant

9. Driver Model

9.1 drivers/drivers.ini

[drivers]
ide.sys
ahci.sys

All listed drivers are loaded unconditionally. Each one's DriverEntry is called after the HAL is initialised. A driver that finds no matching hardware returns STATUS_NOT_FOUND - this is logged via DbgPrint and the kernel continues. Nothing is skipped based on load order.

9.2 Driver Interface

// io_driver.h

#pragma once
#include "hal_api.h"
#include "ke_status.h"

/*
 * Every .sys exports these two symbols.
 *
 * DriverEntry   - probe for hardware, register services, return status.
 * DriverUnload  - cleanup (reserved for future use).
 */
typedef KSTATUS (*DRIVER_ENTRY_FN)(const HAL_API* hal);
typedef void    (*DRIVER_UNLOAD_FN)(void);

/* Disk driver registration - called by a driver inside DriverEntry
   when it has found and initialised a disk controller.
   Exported by hal.dll. */
typedef struct DISK_DRIVER {
    KSTATUS  (*ReadSectors)(uint64_t lba, uint32_t count, void* buf);
    KSTATUS  (*WriteSectors)(uint64_t lba, uint32_t count, const void* buf);
    uint64_t SectorCount;
} DISK_DRIVER;

KSTATUS IoRegisterDisk(uint8_t driveIndex, const DISK_DRIVER* driver);

10. Win32 Personality - win32.dll

The Win32 personality is implemented as a DLL (win32.dll) that provides a user-mode API compatible with Windows. It translates Win32 API calls into equivalent system calls to the kernel. It does not implement the full Win32 API, but provides a subset sufficient for basic applications. The API includes functions for process and thread management, synchronization primitives, file I/O, and basic GUI operations (stubs for now, no graphics yet).


11. Data Structures Reference

Intrusive Doubly-Linked List

// ke_list.h

#pragma once
#include <stddef.h>

typedef struct LIST_ENTRY {
    struct LIST_ENTRY* Fwd;
    struct LIST_ENTRY* Bwd;
} LIST_ENTRY;

#define LIST_INIT(head)    ((head)->Fwd = (head)->Bwd = (head))
#define LIST_IS_EMPTY(head) ((head)->Fwd == (head))

/* Recover the containing struct pointer from an embedded LIST_ENTRY. */
#define CONTAINING_RECORD(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

static inline void ListInsertTail(LIST_ENTRY* head, LIST_ENTRY* node) {
    node->Bwd      = head->Bwd;
    node->Fwd      = head;
    head->Bwd->Fwd = node;
    head->Bwd      = node;
}

static inline void ListRemove(LIST_ENTRY* node) {
    node->Bwd->Fwd = node->Fwd;
    node->Fwd->Bwd = node->Bwd;
    node->Fwd = node->Bwd = node;   /* self-loop: poison against double-remove */
}

12. File Layout

ESP/
├── boot.ini
├── bootx64.efi
├── kernel.exe
├── hal.dll
├── syscall.dll
├── win32.dll
└── drivers/
    ├── drivers.ini
    ├── ide.sys
    └── ahci.sys
    └── fat32.sys

src/
├── boot/
│   ├── main.c              UefiMain, image loading, PML4 setup, MEMORY_MAP conversion
│   ├── ini_parser.c        Minimal boot.ini / drivers.ini reader
│   └── ldr_image.c         symlink to kernel's loader.
│
├── kernel/
│   ├── kmain.c             KernelMain(BOOT_INFO *)
│   ├── boot_info.h         BOOT_INFO structure
│   ├── ke_status.h         KSTATUS codes
│   ├── ke_list.h           LIST_ENTRY, ListInsertTail, ListRemove
│   ├── ke_spinlock.h       SPINLOCK, KeAcquireSpinlock, KeReleaseSpinlock
│   ├── ob_object.c/h       OBJECT, ObAllocateObject, ObReferenceObject, ObDereferenceObject
│   ├── mm_phys.c/h         PMM: MmAllocateFrame, MmFreeFrame
│   ├── mm_virt.c/h         VMM: MmMapPage, MmAllocatePool, MmAllocateUserMemory
│   ├── ke_process.c/h      PROCESS, KeCreateProcess, KeExitProcess
│   ├── ke_thread.c/h       THREAD, KeCreateThread, KeExitThread
│   ├── ke_sched.c/h        MLFQ scheduler, KeSchedulerTick, KeYield
│   ├── ke_switch.asm       KeSwitchContext (MASM64)
│   ├── ke_mutex.c/h        MUTEX, KeAcquireMutex, KeReleaseMutex
│   ├── ke_semaphore.c/h    SEMAPHORE, KeWaitSemaphore, KeSignalSemaphore
│   ├── ke_event.c/h        EVENT, KeSignalEvent, KeWaitForEvent
│   ├── ke_queue.c/h        QUEUE, KeSendMessage, KeReceiveMessage
│   ├── ke_syscall.c        KeSyscallDispatch, syscall table
│   ├── ke_syscall.asm      KeSyscallEntry INT 0x2E stub (MASM64)
│   ├── ldr_image.c/h       LdrLoadImage, LdrResolveImport, LdrGetExport
│   └── memory_map.h        MEMORY_MAP, MEMORY_REGION, MEMORY_TYPE
│
├── hal/
│   ├── hal_main.c          HalInitialize, HalGetApi, HalDebugEnabled
│   ├── hal_debug.c         DbgPrint (printf-style, runtime-gated)
│   ├── hal_screen.c        GOP framebuffer, 8x16 ASCII font
│   ├── hal_keyboard.c      PS/2 + IRQ 1
│   ├── hal_timer.c         HPET / APIC timer fallback
│   ├── hal_pci.c           Mechanism-1 config space
│   ├── hal_disk.c          Drive registry, IoRegisterDisk
│   └── hal_apic.c          Local APIC + I/O APIC, MADT parsing
│
├── syscall/
│   └── syscall_stubs.asm   SysYield, SysAlloc, SysFree, ... (MASM64)
│
├── win32/
│   ├── kernel32.c
│   ├── user32.c
│   ├── ole32.c
│   └── winmm.c
│
└── drivers/
    ├── ide/
    │   └── ide.c
    └── ahci/
        └── ahci.c