Building NexisK: Inside My i386 Kernel, Custom Bootloader and Memory Management
Building an operating system kernel from scratch is not primarily about displaying text on a screen. It is about establishing a controlled execution environment on hardware that initially knows nothing about my kernel.
Before my code can manage memory, receive interrupts, execute processes or provide system calls, it must first solve a more fundamental problem: how to take control of the machine reliably.
That is the problem I am working on with NexisK, an experimental 32-bit x86 kernel built from scratch using C and NASM assembly.
The project currently targets the i386 architecture, uses a custom BIOS bootloader, enters protected mode, discovers physical memory through BIOS E820, initializes interrupt infrastructure and contains the early foundations of physical memory management, system calls and process execution.
This article explains the architecture, the decisions behind it, the current implementation and the engineering lessons I have learned while building it.
The goal is not to present NexisK as a finished operating system. It is to show what exists, what works, what remains incomplete and why each subsystem matters.
Introduction
My current release is NexisK v0.8.9.
The project is organized around a simple principle: implement the mechanisms that an operating system depends on instead of hiding them behind an existing operating-system framework.
NexisK currently includes:
- A custom two-stage BIOS bootloader.
- 32-bit i386 protected-mode execution.
- Global Descriptor Table initialization.
- Interrupt Descriptor Table infrastructure.
- CPU exception handling.
- Programmable Interrupt Controller configuration.
- Programmable Interval Timer support.
- Keyboard and PS/2 mouse interrupt handling.
- VGA text output.
- Serial debugging output.
- Basic system-call infrastructure.
- BIOS E820 physical memory discovery.
- An initial Physical Memory Manager.
- Initial process and context-switching infrastructure.
- A preliminary userspace directory structure.
The kernel is written primarily in C, with NASM assembly used where direct control over processor state and machine instructions is required.
The repository contains 33 commits at the time of writing, and the current release tracks the development milestone v0.8.9.
Source code:
GitHub: NexisK
Why I Built a Kernel from Scratch
There are many ways to build software for a computer. Most application development starts with an operating system that already provides memory management, process isolation, drivers, filesystems and hardware abstractions.
Kernel development starts before those services exist.
I wanted to understand the boundary between software and hardware directly.
What happens when the processor begins executing my boot code?
How does the CPU move from firmware execution into protected mode?
How does a kernel discover which physical memory is available?
How does an interrupt reach the correct handler?
How does a system call cross from a process into kernel code?
These questions are connected. A failure in one layer can prevent every layer above it from working.
For example, a broken GDT can prevent protected-mode execution. An incorrect IDT can cause exceptions to jump to invalid addresses. A faulty memory manager can overwrite kernel structures. A bad context switch can corrupt the execution state of a process.
The architecture must therefore be developed from the bottom upward.
APPLICATIONS
│
▼
USERSPACE
│
▼
SYSTEM CALLS
│
▼
PROCESS MODEL
│
▼
VIRTUAL MEMORY
│
▼
PHYSICAL MEMORY
│
▼
CPU
│
▼
HARDWARE
This is the reason I chose to build the kernel instead of starting with a higher-level operating-system framework.
Architecture
NexisK is a 32-bit x86 kernel. Its current execution environment is i386 protected mode.
The architecture is divided into several subsystems:
NexisK
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
CPU Interrupts Drivers
│ │ │
└────────────────┼────────────────┘
│
▼
Memory Discovery
│
▼
Physical Memory
Management
│
▼
Virtual Memory
│
▼
Processes
│
▼
System Calls
│
▼
Userspace
This is the intended dependency direction, not a claim that every layer is complete.
The current kernel has an initial physical memory manager, while virtual memory and process management remain under development.
Technical Configuration
Component Implementation
Architecture i386 / x86-32
CPU mode Protected mode
Kernel language C
Assembly NASM
Bootloader Custom BIOS bootloader
Boot structure Stage 1 + Stage 2
Memory discovery BIOS E820
Interrupt controller PIC
Timer PIT
Display VGA text mode
Debugging Serial output
System calls int 0x80
Emulator QEMU
Build system GNU Make
License GPL-2.0-only
The kernel is not currently a complete operating-system distribution. It does not provide a complete filesystem ecosystem, production-ready userspace or a Linux-like environment.
That distinction matters because the engineering requirements of a kernel and a complete operating system are different.
The Bootloader
The bootloader is the first major component of NexisK.
Instead of relying on GRUB or Limine, I maintain a separate custom BIOS bootloader responsible for preparing the machine and loading the kernel.
The boot process is divided into two stages.
BIOS
│
▼
Stage 1
│
▼
Stage 2
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
Initialize Boot Menu E820
Environment Selection Detection
│ │ │
└──────────────┼──────────────┘
│
▼
Kernel Loading
│
▼
NexisK
│
▼
kmain
Stage 1 is the initial boot sector. Its job is to begin execution and load Stage 2.
Stage 2 handles the more substantial boot work:
- Boot initialization.
- Kernel selection.
- Memory map detection.
- Kernel loading.
- Transfer of control to the kernel.
The bootloader also generates the bootable disk image and ISO used by the project.
The current disk-image layout is based on a 1.44 MB floppy-style image, which is then packaged as an El Torito bootable ISO.
build/
├── stage1.bin
├── stage2.bin
├── disk.img
├── iso/
│ └── boot.img
└── NexisK.iso
Why Separate the Bootloader from the Kernel?
The bootloader and kernel solve different problems.
The bootloader must understand the firmware environment, load the kernel and provide the information required for kernel initialization.
The kernel must establish its own runtime environment and manage the machine after control is transferred.
Keeping these components separate makes their responsibilities clearer.
It also makes the bootloader reusable. A boot manager should eventually be able to load more than one kernel, expose hardware information and provide a stable interface between firmware and operating-system code.
That is one of the reasons I maintain the bootloader as a separate project.
CPU Initialization and Protected Mode
NexisK targets the i386 architecture and executes in 32-bit protected mode.
Protected mode changes the environment in which the kernel executes. Segment descriptors, privilege levels and interrupt handling become central parts of the system.
The Global Descriptor Table provides the segment descriptors used by the CPU.
The current kernel contains GDT initialization and the basic protected-mode execution environment.
The general transition is:
Real Mode
│
▼
Load GDT Descriptor
│
▼
Enable Protected Mode
│
▼
Far Jump
│
▼
32-bit Kernel Code
The GDT is not an optional decoration. The processor needs valid segment configuration to execute protected-mode code correctly.
This is one of the first places where a kernel stops behaving like an ordinary C program.
The compiler can generate C instructions, but it cannot independently establish the processor environment in which those instructions are valid.
That work belongs to the low-level initialization code.
What I Learned
A kernel cannot treat CPU initialization as a single function that is called and forgotten.
The processor state, descriptor tables and execution mode determine whether the rest of the kernel can run.
A useful debugging strategy is to verify each transition independently:
- Confirm that the bootloader executes.
- Confirm that Stage 2 executes.
- Confirm that the kernel is loaded at the expected address.
- Confirm that protected mode is entered.
- Confirm that the kernel reaches kmain.
- Only then begin debugging higher-level initialization.
This reduces the number of possible causes when the machine stops responding.
Interrupt Architecture
Interrupts are the mechanism through which the processor reacts to events that are not part of the current sequential instruction flow.
NexisK contains infrastructure for CPU exceptions and hardware interrupts.
The main components are:
- Interrupt Descriptor Table.
- CPU exception handlers.
- Programmable Interrupt Controller.
- Programmable Interval Timer.
- Keyboard interrupt handler.
- PS/2 mouse interrupt handler.
- System-call interrupt vector.
The IDT associates interrupt vectors with handler entry points.
The PIC is responsible for managing hardware interrupt requests.
The PIT provides timer events.
Together, these components establish the foundation required for scheduling, input handling and process management.
CPU Exception
│
▼
IDT Entry
│
▼
Exception Handler
│
▼
Kernel Response
For hardware interrupts:
Hardware Device
│
▼
IRQ
│
▼
PIC
│
▼
Interrupt Vector
│
▼
IDT
│
▼
IRQ Handler
Interrupt Vector Configuration
The kernel uses the standard PIC remapping layout:
Master PIC IRQs: 0x20 - 0x27
Slave PIC IRQs: 0x28 - 0x2F
This separates hardware interrupt vectors from the processor exception vectors.
The distinction is important because exceptions and hardware IRQs have different origins and different handling requirements.
The kernel also provides serial output, which is particularly useful when debugging interrupt delivery in QEMU.
VGA output is useful for seeing what is happening on the screen. Serial output is useful for recording execution progress, memory-map entries and diagnostic information without relying exclusively on the display.
Memory Discovery with BIOS E820
Physical memory management cannot begin until the kernel knows which physical addresses are available.
NexisK uses the BIOS INT 15h, E820h interface to discover the physical memory map.
The bootloader collects the memory regions and exposes the resulting map to the kernel.
The kernel then processes the entries and identifies usable regions.
A representative memory map reported by NexisK contains the following entries:
Base: 0x0000000000000000
Size: 0x000000000009FC00
Type: 0x00000001
Base: 0x000000000009FC00
Size: 0x0000000000000400
Type: 0x00000002
Base: 0x00000000000F0000
Size: 0x0000000000010000
Type: 0x00000002
Base: 0x0000000000100000
Size: 0x0000000007EE0000
Type: 0x00000001
Base: 0x0000000007FE0000
Size: 0x0000000000020000
Type: 0x00000002
Base: 0x00000000FFFC0000
Size: 0x0000000000040000
Type: 0x00000002
The two usable regions in this example are:
Region 1:
Base = 0x00000000
Size = 0x0009FC00
Region 2:
Base = 0x00100000
Size = 0x07EE0000
The second region begins at the 1 MiB boundary, which is a common location for usable extended memory in BIOS-based systems.
The E820 memory map is important because physical memory is not necessarily one continuous block.
Firmware-reserved areas, hardware memory regions and other non-usable ranges must not be treated as allocatable RAM.
A physical memory manager that simply assumes every address below the detected RAM limit is free will eventually overwrite something important.
Physical Memory Manager
The Physical Memory Manager is one of the most important subsystems currently being developed in NexisK.
Its responsibility is to track physical pages and provide the foundation for future virtual memory management.
The current implementation consumes the E820 map and identifies usable physical memory regions.
BIOS E820
│
▼
Memory Map Entries
│
▼
pmm_init()
│
▼
Filter Type 1 Regions
│
▼
Calculate Pages
│
▼
Calculate Page Addresses
│
▼
Calculate Bitmap Index
│
▼
Mark Usable Pages
The PMM processes usable regions in increments of 4 KiB.
That page size is important because it is the standard x86 page granularity used by the paging architecture.
The current bitmap representation uses one byte per physical page.
Bitmap Entry:
0 = Free
1 = Reserved / Occupied
Each bitmap entry corresponds to one 4 KiB physical page.
This is an initial representation, not the final memory-management design.
Bitmap Overhead
A byte-per-page bitmap is simple to understand, but it consumes more metadata than a packed bit bitmap.
For example, managing 1 GiB of physical memory with 4 KiB pages requires:
1 GiB / 4 KiB = 262,144 pages
With one byte per page:
262,144 bytes = 256 KiB
With one bit per page:
262,144 bits = 32 KiB
The byte-per-page representation therefore uses eight times more metadata than a one-bit-per-page representation.
I chose the simpler representation for the initial implementation because it makes page-state inspection and debugging straightforward.
Memory efficiency matters, but correctness matters first.
The current PMM still requires additional work for:
- Complete bitmap initialization.
- Kernel memory reservation.
- Bootloader memory reservation.
- Bitmap memory reservation.
- Physical page allocation.
- Physical page freeing.
- More precise handling of memory-map boundaries.
- Integration with the Virtual Memory Manager.
Lessons from Physical Memory Management
The biggest lesson is that discovering memory and managing memory are different problems.
E820 tells the kernel which regions exist and which regions are usable.
The PMM must then maintain the state of individual physical pages.
That means it must know which pages are free, which are reserved and which are currently allocated.
The PMM cannot safely allocate memory until its own metadata is protected.
This creates a dependency:
Memory Discovery
│
▼
PMM Metadata
│
▼
Reserve Kernel and Boot Memory
│
▼
Physical Page Allocation
│
▼
Virtual Memory
The metadata used to manage memory is itself stored in memory. The kernel must therefore reserve and protect its own bookkeeping structures.
This is one of the first places where operating-system development becomes a problem of managing the resources used to manage resources.
Virtual Memory and Paging
The Virtual Memory Manager is planned to be built on top of the physical memory manager.
The current VMM is not considered complete.
The intended functionality includes:
- Paging.
- Virtual address mapping.
- Page fault handling.
- Dynamic page mapping.
- Kernel/user memory permissions.
- Per-process address spaces.
The relationship between physical and virtual memory is fundamental.
The PMM tracks physical pages.
The VMM controls how virtual addresses map to those physical pages.
Virtual Address
│
▼
Page Tables
│
▼
Physical Address
│
▼
Physical Memory
Without a working physical memory allocator, the VMM cannot reliably create new mappings.
Without a working VMM, process isolation and independent address spaces are difficult to implement correctly.
This is why I am treating memory management as a staged subsystem instead of trying to implement every feature at once.
Process Infrastructure
NexisK contains initial process-related infrastructure and context-switching groundwork.
This is not yet a complete multitasking system.
The intended process subsystem includes:
- Process creation.
- Process destruction.
- PID management.
- Process address spaces.
- Context switching.
- Scheduler.
- Preemptive multitasking.
- Process isolation.
- Userspace execution.
A process is more than a function call.
A process requires an execution context, a stack, an address space and a mechanism for switching between execution contexts.
A simplified process transition looks like this:
Current Process
│
▼
Save CPU Context
│
▼
Select Next Process
│
▼
Restore CPU Context
│
▼
Next Process
The context-switching infrastructure is only one part of the complete process model.
A scheduler must decide when a process runs. The memory subsystem must provide the required address-space behavior. The interrupt subsystem must provide a mechanism for preemption or other scheduling events.
This is another example of why kernel subsystems cannot be developed as completely isolated features.
They have interfaces, but their correctness depends on the behavior of the other components.
System Calls
NexisK contains a basic system-call mechanism using the int 0x80 instruction.
The syscall number is passed through the EAX register.
The current interface is intentionally minimal and is primarily used to validate the system-call path.
Execution Context
│
▼
int 0x80
│
▼
IDT[0x80]
│
▼
Syscall Handler
│
▼
Kernel Syscall
│
▼
iret
The system-call mechanism establishes the basic path from an execution context into kernel code.
The interface will need to evolve alongside process management, privilege levels and userspace support.
A production-quality syscall ABI requires much more than a single interrupt vector.
It needs defined calling conventions, argument validation, return values, error handling and a stable interface between user programs and the kernel.
The current implementation is the beginning of that path, not a complete syscall subsystem.
Kernel I/O and Drivers
The kernel currently provides basic low-level I/O through:
- VGA text output.
- Serial output.
- Keyboard input.
- PS/2 mouse input.
These drivers are small, but they are essential for making the kernel observable.
A kernel without reliable output is difficult to debug.
A kernel without input cannot meaningfully interact with a user.
The VGA driver provides visible text output in the traditional text-mode environment.
The serial driver provides diagnostic output that can be captured externally.
The keyboard and mouse handlers provide the first layer of device interaction.
The current hardware support is deliberately limited. Storage drivers, filesystem support and additional device drivers remain future work.
Real Hardware Testing
QEMU is extremely useful for kernel development, but it is not a replacement for physical hardware.
An emulator provides a controlled environment. It allows me to reproduce boot behavior, inspect execution and collect diagnostic information.
NexisK uses QEMU as its primary emulation environment and also includes real hardware boot validation in its development process.
The build system provides:
make
to build the kernel and bootable image.
To run the kernel in QEMU:
make run
For deeper QEMU diagnostics:
make dev
The debug configuration writes additional information to:
build/qemu.log
This is useful for investigating:
- CPU resets.
- Interrupt behavior.
- Guest errors.
- Unimplemented instructions.
- MMU activity.
- Protected-mode execution.
- Kernel execution.
Why Hardware Testing Matters
A kernel can behave correctly in an emulator and still fail on a physical machine.
Firmware implementations differ.
Hardware initialization differs.
Memory maps differ.
Devices differ.
The boot path may encounter assumptions that were never exposed in the emulator.
For NexisK, this matters particularly because the bootloader interacts directly with BIOS services and hardware.
Real hardware testing is therefore not merely a demonstration that the kernel boots. It is a way to discover assumptions about the machine that are not visible in a controlled emulated environment.
The current project documentation records real hardware boot validation, but it does not provide a complete compatibility matrix across different machines.
I do not consider the current bootloader universally compatible with all x86 hardware.
Performance
NexisK is currently a kernel-development project, not a performance benchmark suite.
The repository does not currently provide measured syscall latency, interrupt latency, context-switch timing, memory-allocation throughput or boot-time benchmarks.
I will not invent those numbers.
The current measurable implementation details are architectural:
Metric Current Value
Target architecture i386
CPU mode 32-bit protected mode
Page size 4 KiB
PMM bitmap entry size 1 byte
Boot stages 2
Syscall vector 0x80
Master PIC vector range 0x20–0x27
Slave PIC vector range 0x28–0x2F
Current release v0.8.9
Repository commits 33
These values describe the implementation. They are not performance results.
A meaningful performance evaluation will require a defined workload and a stable implementation.
For example, a future PMM benchmark could measure the time required to allocate and free a fixed number of physical pages.
A syscall benchmark could measure the cost of entering the kernel through int 0x80 and returning through iret.
A context-switch benchmark could measure the time required to save and restore process state.
Those measurements would become meaningful only after the relevant subsystems are complete enough to benchmark.
Development Lessons Learned
- Bootloader Complexity Is Not the Same as Kernel Complexity
A kernel can have a working C entry point and still be unable to boot reliably.
The bootloader must establish the environment in which the kernel executes.
That includes loading the kernel correctly, preserving required information and transferring control at the correct processor state.
The bootloader deserves its own design and testing.
- Memory Management Must Be Developed Incrementally
The PMM cannot be treated as a single feature.
Memory discovery, page tracking, reservation, allocation and freeing are separate responsibilities.
Trying to implement all of them simultaneously makes debugging much harder.
The current byte-per-page bitmap is intentionally simple. It allows the initial implementation to focus on page-state tracking before optimizing metadata representation.
- Hardware Abstraction Must Not Hide the Hardware Too Early
The current kernel uses explicit architecture-specific code.
That is appropriate for an i386 kernel because the processor’s descriptor tables, interrupt vectors and I/O mechanisms are directly relevant to the implementation.
A large abstraction layer introduced too early can hide the exact state that needs to be debugged.
I prefer to establish correct low-level mechanisms first and abstract them only when the interfaces are understood.
- A Directory Is Not an Implementation
NexisK includes:
userspace/
├── init/
└── shell/
These directories establish the intended separation between kernel code and future userspace programs.
They do not mean that Ring 3 execution, executable loading or a complete shell already exist.
The same principle applies to process management and virtual memory.
A subsystem should be described according to what it actually implements, not according to what its directory structure suggests.
- QEMU and Real Hardware Solve Different Problems
QEMU is valuable for repeatable testing and diagnostics.
Physical hardware exposes firmware and compatibility assumptions.
Neither environment completely replaces the other.
A useful development process uses emulation for rapid iteration and hardware testing for validation against real machines.
- Kernel Development Is Dependency Management
The kernel architecture is a dependency graph.
The bootloader provides the environment.
The CPU subsystem establishes execution.
The interrupt subsystem provides event handling.
The memory subsystem provides allocation and address translation.
The process subsystem depends on memory and interrupt behavior.
System calls depend on the execution and privilege model.
Userspace depends on all of them.
Understanding these dependencies prevents implementing higher-level features on unstable foundations.
Future Development
The next stages of NexisK development are centered on completing the existing architectural foundations.
Bootloader
The bootloader needs more robust disk access, improved hardware compatibility and eventually UEFI support.
The current design is based on the traditional BIOS environment.
A future x86-64 transition will also require changes to the boot architecture.
Memory Management
The PMM needs complete bitmap initialization, proper reservation handling, physical page allocation and freeing.
The VMM needs paging, page fault handling and dynamic mapping.
These are prerequisites for a reliable process model.
Privilege Levels
The kernel currently has protected-mode infrastructure and initial work toward privilege-level support.
Full Ring 3 execution and kernel/user separation remain under development.
The final privilege model must provide a reliable boundary between kernel code and userspace code.
Processes and Scheduling
The process subsystem needs a complete lifecycle, address spaces, context switching and scheduling.
Preemptive multitasking will require coordination between the timer, interrupt handling, process state and memory management.
Userspace
The initial userspace directory structure exists, but the actual execution environment remains to be implemented.
Future work includes:
- Executable loading.
- Initial init process.
- Userspace system-call API.
- Shell execution.
- User-space programs.
- Process isolation.
Storage
Storage support is not currently available as a complete kernel subsystem.
Future work includes disk abstraction, disk drivers, LBA support, filesystem abstraction and file operations.
Get Started
NexisK is currently developed and tested primarily on Linux.
The required tools are:
- GCC.
- NASM.
- GNU Make.
- GNU ld.
- QEMU.
- genisoimage.
On Debian or Ubuntu:
sudo apt update
sudo apt install \
build-essential \
gcc \
nasm \
make \
binutils \
qemu-system-x86 \
genisoimage
Clone the repository:
git clone https://github.com/icarotelesdasilva/NexisK.git
cd NexisK
Build the kernel and bootable ISO:
make
The generated files are placed in:
build/
The main bootable image is:
build/NexisK.iso
Run the kernel with QEMU:
make run
For deeper debugging:
make dev
To remove generated build artifacts:
make clean
A clean build can then be produced with:
make
Conclusion
NexisK is an ongoing effort to build a 32-bit x86 kernel from the lowest levels upward.
The project currently has a custom BIOS bootloader, protected-mode execution, interrupt infrastructure, hardware input, serial debugging, BIOS E820 memory discovery and an initial physical memory manager.
It also contains the early foundations of system calls, process management and userspace architecture.
The most important part of the project is not the number of features. It is the relationship between them.
The bootloader must load the kernel correctly.
The CPU must execute in a valid environment.
The interrupt subsystem must deliver events reliably.
The memory manager must track physical pages without corrupting kernel state.
The process subsystem must preserve execution contexts.
The syscall interface must establish a controlled path into the kernel.
Each layer depends on the previous one.
That is what makes kernel development difficult, and that is also what makes it useful.
I am building NexisK to understand these mechanisms directly, with the implementation exposed instead of hidden behind a finished operating system.
The project is still evolving. Its current limitations are part of the engineering work, not something to conceal.
If you are interested in operating-system development, x86 architecture, C, assembly programming or bare-metal systems programming, the source code is available.
Explore the project, inspect the implementation and follow the development.
- GitHub: NexisK
- GitHub profile: icarotelesdasilva
If you are working on your own kernel, I would be interested in comparing implementation approaches, discussing memory management or examining how different kernels handle the hardware/software boundary.
The source is open. The architecture is still being built.
And the next layer is always waiting.
Top comments (0)