How To Implement Your Own Libc

Our OS doesn’t have libc support, so let’s work on that!

Kell
Better Programming
4 min readAug 26, 2022

--

Let’s dive into OS development together!

This is the second part in a series I’m writing about creating your own operating system. You can follow along through the repository on GitHub (or Sourcehut).

When we left, we had set up a basic shell that would greet us whenever we booted into our system. There wasn’t much else to it beyond the written Assembly code and the bits and pieces of C such as VGA colour codes and low-level printing. In this post, we’ll expand on our shell program and implement some key libc functionality.

First things first, we’ll need to create some files to hold our code. You may call them whatever you like, but I created a basic source tree that looks something like this:

  • / (root directory)
  • /main — Contains the entry point and our shell program.
  • /lib — Contains our “libraries” for use throughout the application.
  • /lib/boot — Contains our bootloader.
  • /lib/libc — Contains our libc implementation.
  • /lib/link — Contains our linker scripts.

NOTE: This layout might not be your favorite, and you’re welcome to set up your source tree as you’d like. This is what helps keep me organized.

You’ll want to create at least two header files to contain the definitions of MemCpy, MemSet, MemCmp, MemMove, strlen, abort, and PutChar. These are a few basic functions that enable us to expand our kernel further. We will start with the mem functions:

Define a header file to contain our mem functions.

Now we’ll implement those functions in their own file. Here’s the code:

Very basic memory-utility function implementations.

We now have some basic memory manipulation tools at our disposal, but we need a proper ‘print’ implementation and some of its incarnations. To accomplish this, I want to take a slight detour from the libc implementation and create something like a software UART (universal asynchronous receiver-transmitter).

To start, we’ll create a struct called ‘Uart’ and give it one field: the base memory address. We will add more to this structure later on, but for now, we’ll write it out with a single field and add two functions alongside it. Here’s what that looks like:

A basic universal asynchronous receiver-transmitter system.

Then we implement these functions, as shown below:

UART functions implemented.

Our UartInit function creates a pointer to an instance of the Uart structure. This pointer will be passed to the other related functions — Read and Write.

In our Read function, we have two parameters: the Uart pointer and an integer offset. Shifting the focus to the function body, we can see that we declare a pointer to a volatile, unsigned 8-bit integer and set it to the base address in our struct. We use ‘volatile’ here so that the compiler does not optimize away our code. Finally, we dereference and return the combined value of the volatile pointer and the offset.

To write our value to the UART, we take a pointer to the Uart and an offset, much like Read, but we also take an unsigned 8-bit integer value to write. We use ‘volatile’ in the same manner as before, assigning our value to the combined pointer and offset.

Now that we have a very basic memory-mapped IO setup, we can get back to our stdio headers. We will rewrite our shell functions to use our UART definitions later on. For now, let’s write out our Print and Printf functions:

Let’s define our printing functions!

After our include guard, we pull in from a header I haven’t talked about yet called sys/cdefs.h. This is a file I created to house some of my personal #defines and type aliases and such. It’s not that important as there’s currently only one definition in that file beyond its own include guard, so we will revisit it in a later installment.

Next, we implement our functions with the following code:

There is a lot to unpack here, so we will start with the PutChar function. This takes an integer to represent a “character code” and then converts that into a char before feeding it to the UartWrite function that then performs a volatile write. For now, this function initializes the UART on its own, but we will change that later.

The static Print function takes a character array and simply loops until it puts each character supplied through the PutChar function, though Printf gets necessarily more complex.

Because we’re trying to accomplish some basic formatting, we must get creative: we need to check for some commonly used special formatting characters.

Firstly, we have to make sure we’re not writing nothing, so we check for our EOF condition, and then we move on to check whether we have encountered a ‘format expression.’ These start with the % symbol, and the following character denotes the type of formatting we’ll be doing: %s for string formatting, %c for character formatting. Another time, we will implement formatting for integers, floats, and other data types.

Now we have one more function to implement: abort.

Abort halts the program at an error!

This function is called when our system encounters an unrecoverable error. Ideally, we should never reach this point, but we need the ability to perform a fast halt of the system should we get there.

Now that we have some basic libc functionality, we may compile our program as last time, and we can type away! We don’t yet have a “shell” like Bash or ZSH, but we’ll work on implementing something more proper in the next installment, so stay tuned!

Resources

As a disclaimer, some of the code was borrowed or translated from other sources:

My git repositories:

The story so far:

Once again, I very much appreciate you reading my story, and I look forward to the next time we meet! ❤

Sign up to discover human stories that deepen your understanding of the world.

--

--

No responses yet

Write a response