The Care and Feeding of Interrupt Handlers
Dear diary,
Today I decided to give my embryonic operating system a voice. Not literally—that would be terrifying—but an interactive shell so it could at least complain about its existence in real time. What started as an innocent afternoon project turned into another reminder that hardware and I have what therapists might call "trust issues."
The plan seemed reasonable enough: create a simple command prompt that could poke around the system hardware. You know, basic things like "what PCI devices do we have" and "where did all my memory go." The sort of diagnostic tools that separate a proper OS kernel from what is essentially a very expensive screensaver.
I'd already conquered the Herculean task of drawing pixels to the screen—my proudest achievement to date—so surely adding keyboard input couldn't be that much harder. The UEFI specification even provides nice abstractions for this sort of thing. Just read from ConIn, echo characters back, handle a few special keys. How hard could it be?
Narrator: It was exactly as hard as he expected, which somehow made it worse.
The shell implementation started promisingly. I created a neat little input loop that waits for keyboard events, echoes printable characters, and handles backspace with the sort of careful buffer management that would make my CS professors weep with pride. The command parser splits input on spaces and dispatches to handler functions. Clean, simple, maintainable code.
The first command I implemented was 'help', because I am fundamentally unoriginal. Then 'clear' to wipe the screen, because watching text scroll off into the void makes me existentially uncomfortable. Then 'reboot' and 'halt', because sometimes you need an escape hatch when your creation develops delusions of grandeur.
But the real meat was going to be hardware inspection. I wanted my OS to survey its domain like a new homeowner checking what's actually in the basement. This meant implementing PCI bus scanning—the digital equivalent of opening every cabinet and drawer to see what's inside.
PCI scanning is one of those things that sounds impressive but is really just systematic brute force. You iterate through every possible bus, device, and function combination, poking at I/O ports 0xCF8 and 0xCFC to see if anything responds. It's like knocking on doors in a very large, mostly empty apartment building, except some of the residents are graphics cards with anger management issues.
The beautiful thing about PCI is that it's completely standardized. Every device announces itself with a vendor ID, device ID, and class code. I built a little database of known hardware—Intel chipsets, NVIDIA graphics, Realtek network controllers—so my OS could recognize the residents of its new digital neighborhood.
Testing this on my Aorus X3 laptop was like introducing two socially awkward people at a party. My kernel politely requested a list of PCI devices, and the hardware responded with what I can only describe as a comprehensive résumé of every chip on the motherboard. Intel Haswell DRAM controller at 00:00.0, HD Graphics 4600 at 00:02.0, SATA controller at 00:1F.2—a complete census of silicon citizens.
But wait, there's more. Because I apparently enjoy complications, I also implemented memory map inspection. UEFI kindly provides a GetMemoryMap function that tells you exactly how your system's RAM is carved up. Conventional memory here, boot services there, runtime services over there, and a suspiciously large chunk marked "Reserved" that's probably hiding something important.
The memory display shows you the harsh reality of modern computing: out of your gigabytes of RAM, a shocking amount is already spoken for before your OS even gets to first base. Boot services code, ACPI tables, MMIO regions—it's like buying a house and discovering the previous owner left all their furniture with notes saying "DO NOT MOVE."
When I finally built the whole thing, the output was satisfying in that uniquely developer way:
[CC] src/lib/shell.c
[CC] src/lib/pci.c
[LD] deploy/BOOTX64.EFI
Build complete. Output: deploy/BOOTX64.EFI (27,648 bytes)
From 21KB to 27KB. My kernel was growing up, developing personality. Or at least the ability to introspect about its own existential crisis.
The moment of truth came when I booted the USB drive on real hardware. The familiar ChronOS banner appeared, my console initialized properly, and then... a command prompt. Blinking cursor. Waiting for input.
I typed 'help' and held my breath.
The command list appeared instantly. No crashes, no triple faults, no mysterious reboots. Just a clean list of available commands like a proper operating system might display. I felt that peculiar mixture of relief and disbelief that accompanies any successful interaction with x86 hardware.
The 'pci' command was the real test. When I pressed enter, I could practically hear the kernel methodically knocking on every door in the PCI address space. A moment later, a neat table appeared:
Bus:Dev.Fn Vendor:Device Class Description
00:00.0 8086:0C00 06:00 Intel Haswell DRAM
00:02.0 8086:0416 03:00 Intel HD Graphics 4600
00:1F.2 8086:8C03 01:06 Intel 8 Series SATA (AHCI)
My OS was actually talking to the hardware and getting sensible responses. It knew about the integrated graphics, the SATA controller, even the wireless adapter. Like finally having a proper conversation with someone you'd only exchanged awkward nods with in the hallway.
The 'mem' command revealed the brutal economics of system memory:
Type Start Pages Size
Conventional 00100000 156238 629 MB
BootServicesData 26F64000 8 32 KB
Reserved FED00000 1024 4 MB
Out of several gigabytes, only about 629MB was marked as "conventional"—free for my OS to use however it pleased. The rest was a complicated web of reservations, runtime services, and hardware mappings. Like discovering your new apartment comes with seventeen different HOA fees.
The logging system faithfully captured everything in UTF-16 format on the USB drive. When I decoded BOOTLOG_002.txt later, I could see the complete conversation between my shell and the hardware, preserved for posterity like a transcript of humanity's first contact with an alien civilization. Except the aliens were PCI devices and they mostly wanted to talk about their vendor IDs.
What struck me most was how routine it all felt once it worked. The shell accepted commands, parsed them correctly, executed the appropriate functions, and displayed results. The very definition of "working as intended." No drama, no last-minute debugging sessions, no mysterious crashes at 2am.
This is perhaps the strangest part of OS development: the profound satisfaction when something mundane actually works. I'd built an interactive shell that could inspect its own hardware environment. Not exactly revolutionary—every operating system since the 1970s has had this capability—but mine did it too, and that felt like joining a very exclusive club.
The PCI scanner found every device on the motherboard. The memory mapper correctly interpreted UEFI's memory layout. The command parser handled edge cases gracefully. All the careful buffer management and input validation paid off in boring, reliable functionality.
As I typed 'halt' to end the session, watching my OS politely save its log file and shut down the CPU, I realized I'd crossed another small threshold in this quixotic project. My kernel could now have conversations, even if they were mostly about hardware specifications and memory addresses.
Next comes the real challenge: storage. The SATA controller is sitting right there at 00:1F.2, advertising its AHCI capabilities like a résumé. Time to teach my OS to read and write actual data, to persist information beyond the fragile boundary of system reboot.
But that's tomorrow's problem. Tonight, I have a working shell and a sense of cautious optimism that I haven't completely lost my mind building an operating system in 2026.
The blinking cursor awaits.
Top comments (0)