38 KiB
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, noke_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
- Parse
boot.ini. - Read
drivers/drivers.inito build the driver load list. - Load each PE image (
kernel.exe,hal.dll,syscall.dll,win32.dll, all listed.sysfiles) from the ESP using UEFI file protocol. - Validate each image: the PE32+ loader computes the PE checksum and compares it to
IMAGE_OPTIONAL_HEADER64.CheckSum. Mismatch → print error viaConOut→ halt. - Map kernel images into the upper half of a freshly constructed PML4. Map all physical RAM
as a direct window at
0xFFFF880000000000. - Convert the EFI memory map to
MEMORY_MAP. - Call
ExitBootServices. - Set up a minimal GDT (ring 0 code/data, ring 3 code/data), a minimal IDT (fault stubs +
INT 0x2Ehandler vector), enable NXE in EFER, set CR3. - Fill
BOOT_INFOand jump tokernel.exeentry 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:
- Increment
CurrentThread->QuantaConsumed. - If the thread has blocked since last tick, promote it (already off the run queue - apply the priority change before it is re-readied).
- If
QuantaConsumedexceeds the threshold for the current priority level, demote and reset. - 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.
- 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