Kernel Sanders: Building a Unix Kernel from Scratch

Collecting the Herbs and Spices of Operating Systems

Taking Inventory: The Background and Motivation for Kernel Sanders

As part of my undergarduate degree at the University of Illinois at Urbana-Champaign, one of my core classes, Computer Systems Engineering, also known as ECE391, was a hands-on computer engineering course that covered many of the core components behind OS-level engineering. The final project, known as Machine Problem (MP) 3, put our knowledge to the test in a multi-stage endeavor that involved building a Unix kernel in x86 assembly language and native C in groups of four. When our group came together, we promptly chose the name "Kernel Sanders" to represent our project.

As this course was a prerequisite for later computer security courses, I opted to take it as soon as possible. This was the first of the "Big Four" ECE courses I opted to take, and from what I gathered from student reviews, it was a difficult place to start. I was able to experience this for myself from a team leadership and project management perspective; my team and I spent many evenings in the 2nd floor of the ECE building attempting to piece together bits of code to make our kernel come to life.

When I took this course in 2022, MP3 was split into five checkpoints with deadlines placed over the course of two months. The checkpoints were roughly organized as such:

I will admit that this project was challenging, and this story is one about failure. To write about my experience may seem counterintuitive, but these are my highlights from this experience:

Setting the Stage: Bootstrapping Kernel Sanders

At the beginning of the project, our first task was to bootstrap the kernel. The project repository, hosted on GitHub and accessible to all team members, contained some placeholder files that were designed to interface with QEmu, an emulator service that hosted the environment for our kernel. This step required writing some basic assembly linkage in x86 that tethered QEmu to our kernel. This step was critical as this is the only way we can interface with our kernel outside of the Visual Studio development environment, and it was the primary platform that would be used for testing by TAs and professors at each checkpoint's deadline.

Logic diagram of the i8259 chip

Another task for this checkpoint was to develop drivers for the primary devices that would interface with our kernel, such as they keyboard and the Programmable Interrupt Controller (PIC) chip (i8259). The PIC chip allowed us to throw interrupts to our kernel, which allowed the CPU to respond to requests from other devices, such as the keyboard, monitor, and Real-Time Clock (RTC) among other devices. Our usage of the i8259 mimicked a device-based polling solution that has been used in real-life computer architecture implementations; for example, this chip and its variations (i8259A) served as the interrupt controller for the Instruction Set Architecture (ISA) bus in the original IBM PC. The i8259 chip was a critical component for our Interrupt Descriptor Table (IDT), a space in memory used by the processor to determine where to move the stack pointer when an interrupt is thrown.

Another requirement for our team in Checkpoint 1 was a basic implementation of our paging scheme. The paging scheme was a difficult item for us to implement, as it required a solid understanding of referencing and dereferencing values in memory and keeping track of the stack pointer. While it had been stressed in previous MPs, this is where our team really started to understanding the GNU Debugger (GDB) command-line tool. I believe GDB is one of the best ways to step through programs in Unix, and it still stands as one of my core tools when performing reverse engineering tasks for Unix systems (Ghidra aside).

For each checkpoint, we used a boot image (mp3.img, frequently refreshed each time our OS crashed) and a testing file (tests.c, frequently modified for unit testing). Keeping track of these files helped us maintain progress and check off required tasks for each checkpoint, which became critical toward the end of the MP.

Developing the Blend: Implementing the Filesystem

For the second checkpoint, the first challenge our team faced was implementing the terminal driver. The terminal driver is a critical part of our application; without the terminal, we wouldn't be able to interface keyboard input or run programs from the filesystem, two main components of any kernel. As such, our testing scheme for the terminal driver included reading input from the keyboard and writing strings to the terminal.

In our write test, we encountered issues with writing long strings to our terminal. With help from some TAs, we discovered that this issue had to do with how our terminal handled wrapping text between lines, which tied back to some of our driver code and monitor interface from the first checkpoint. This was a true exercise in finding and squashing bugs as they appear in development, a valuable skill which I use often and will likely continue to use as my career progresses.

The next challenge for our team was implementing a read-only filesystem. The filesystem is another critical element of our Unix kernel; without it, users would not be able to store and retrieve information through the terminal driver. This enables users to run programs and browse the filesystem directory, among other critical tasks.

Testing for the filesystem involved checking if a file exists (test), printing the content of different sized files (cat), and printing a list of all files in the filesystem (ls). Coupled with the issues we encountered in our terminal driver, testing the filesystem was a more difficult endeavor than we anticipated, but by working together as a team we were able to complete all of our objectives by the second deadline.

Mastering the Recipe: Developing Syscalls

Animated image of the Pingpong program

For the third and fourth checkpoints in our project, our team was tasked with implementing System Calls (syscalls) in our kernel. Syscalls are methods that programs use to interface with our kernel and the underlying operating system.

In the third checkpoint, we integrated some traditional programs into our kernel such as shell and hello, a program that asks for the user's name and repeats it back in the manner "Hello, <name>!". Some additional writing tests we needed to accommodate for our syscalls include counter, a program that prints numbers in the terminal from 0 to 1000, and pingpong, a program that uses the RTC to "bounce" a letter between two pipes. A visual demonstration of pingpong can be seen alongside this paragraph.

The fourth checkpoint took place shortly after Thanksgiving break, which the school observed as a holiday. The requirements were lower than the previous checkpoints, but still included some important items that were necessary for the creation of our kernel.

Perhaps the most important part of this checkpoint was the integration of video memory (vidmem) into our kernel. Given the troubleshooting we had already conducted regarding assembly linkage and the filesystem, the incorporation of vidmem into our project is where we became adept at using GDB to dissect our code. Much like the third checkpoint, the test programs we used to verify our progress were launching multiple shells (shell > shell), incorporating arguments into commands (cat <filename>), and a quick two-frame animation test to ensure vidmem is mapped correctly (fish).

This checkpoint was where our pacing started to suffer; not only were our other classes starting to pick up and finals began to loom near, but we had lost a member of our team and we weren't quite sure how to allocate the work between the three of us that remained. This resulted in more hours in the lab, more research on the internet, and many trips to the Taco Bell on University Street to keep us fueled through it all.

Plating it Up: Multiple Terminals and Scheduling

At the final checkpoint, teams can incorporate additional features for extra credit. Our team found this checkpoint to be the most challenging part of the project, not necessarily for what we wanted to do but how we needed to bring everything together. With final exams and other projects impending, our team found it difficult to truly come together and deliver the final product. This turned into a great learning experience for the three of us; we learned how to efficiently manage our time when we needed to deliver MVPs.

We were able to implement a quick round-robin scheduling algorithm for our kernel but fell short when it came to implementing multiple terminals. The efficiency of the schedulign algorithm came in the form of a slightly more reponsive terminal, which mattered when it came to programs like fish and pingpong. For the multiple terminal setup, the idea is that you can toggle between three terminals by using the function keys (terminal 1 on F1, terminal 2 on F2, etc.) Looking back on this, it might have been a memory or paging error, but we were not able to retain context on the terminals when we switched between them. For example, the output of counter would disappear if we tabbed in and out of the terminal.

Wrapping Up: A Final Review

This project was my first true endeavor into operating systems and kernel creation, and there's no better way to do this than from scratch. Working as a team helped split the load, but by the time we lost our fourth team member we ultimately struggled with how to keep up the pace and reach the finish line. Regardless, this was a learning lesson for all of us in time management, project management, and sifting through code figuring out what went wrong.

The lessons learned through this project have definitely helped me with future classes, future work projects, and future personal projects. The ability to really dig into how the kernel works, especially when I helped write the kernel, really aided in my understanding of how to use GDB (and how to debug in general). I find that I am able to perform some of my best work under pressure, so the added stress of completing Checkpoint 5 while finals loomed near helped me a little bit at the end. And finally, working as a team became very important with this project; it became critical for later projects, both in and out of school.

Overall, this project was a great way to develop skills in OS programming and architecture. This project served as one of the main reasons why I wanted to get into software engineering and low-level architecture, and later courses helped solidify my sentiment.

Photo Sources

OpenAI: header image

EEEGuide.com: i8259 pinout

Published Apr. 2025