Build your own OS (Part 2)

Pasan Devin Jayawardene
5 min readJul 21, 2021

--

Start implementing with C

Hello everyone,

Welcome to the second part of my article series on developing an operating system. Last week in my first article, I explained to you how to set up the development environment and boot the simplest OS that we can build. If you haven’t already read that article, I strongly advise you to do so because you won’t be able to understand this article otherwise. Following is the above-mentioned article.

What we did last week

Figure 2: Title

Okay. First of all, let’s remind some things from the last week in order to continue the conversation. As mentioned above, the main things we did are setting up our OS development environment and booting a simple OS. For that, We used Ubuntu as our host operating system and Bochs as the virtual machine. We used Assembly as the main language to write the OS kernel. The only thing that OS did was putting a special number cafebabe in the eax Register.

What PL is better for our project?

Now let’s talk about programming languages. Till now we used Assembly as our PL for writing the kernel. Assembly is excellent for interacting with the CPU and provides complete control over all aspects of the code, but that’s not as convenient as the C programming language. It is easier to use C as much as possible in this project because there are lots of code that we have to write for our kernel and it’s not an easy task to write all of them in assembly.

Figure 3: Choose your PL wisely

In this article, I’ll show you how to write a code in C for our kernel and call that from the Assembly code. So we can use C to develop our OS in future and use Assembly only when it is needed.

Creating a Stack

Figure 4: Title

Since all non-trivial C programs use a stack, it is a prerequisite for using C. To set up a stack we need to point the esp register to the end of a correctly aligned area of free memory.

In the kernel’s ELF file, we reserve a piece of uninitialized memory in the bss section. Because GRUB understands ELF, it will allocate any memory reserved in the bss section when the OS is loaded. To declare uninitialized data, we use the NASM pseudo-instruction resb.

Copy the following code in the file loader.s, right after the last definition (CHECKSUM).

Code 1: Reserving memory for the stack

Then set the stack pointer by pointing esp register`to the end of the kernel_stack memory. To accomplish this, replace the instruction in the loader label of the loader.s file with the following code.

Code 2: Setting up the stack pointer

Our first function using C language

Following is the first C function that we are going to call from our assembly code. It will take three integer arguments and return the sum.

Copy the following code to a new file named kmain.c. This will be the source file of our C code.

Code 3: C function to add three integers

Using Assembly to call a C function

Just writing a C function is not enough. We need to call it using assembly.

We will use the cdecl calling convention because it is the one used by GCC. According to the cdecl calling convention, arguments to functions should be passed through the stack. Consequently, the arguments should be pushed to the stack in right-to-left order, with the rightmost argument being pushed first. The function’s return value will be stored in the eax register.

To accomplish the task, place the following code after the esp instruction in the loader.s file.

Code 4: Calling C function from Assembly

Compiling C code

When compiling the C code for the OS, a lot of flags to GCC need to be used. This is due to the fact that the C code should not assume the presence of a standard library, as there is none available for our operating system. In addition, we should enable all warnings and treat them as errors.

When compiling the C code, we use the following flags to achieve the aforementioned goals (We will use these in the Makefile which we are going to create in the next step):

-m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector 
-nostartfiles -nodefaultlibs -Wall -Wextra -Werror

Set up Build Tools

Now we’ll set up some build tools to make compiling and testing our operating system easier. We will use make as our build system.

Here’s a simple Makefile for our OS:

Code 5: Makefile configuration

Create a file named Makefile and save the above text in that file.

Our working directory should now contain the following files:

Figure 5: working directory

Following is the loader.s file after all the modifications have been done:

Code 6: loader.s file after the modifications

Since we configured the Makefile, you should be able to start the OS with the simple command make run, which will compile the kernel and boot it in Bochs. Quit Bochs and display the log generated by Bochs with the cat bochslog.txt command (just like we did before). You should see number 6 in the eax register. That is because the function has returned number 6 (sum of the arguments we provided).

Figure 6: Final output (Number 6 on eax register)

I am linking my implementation for your convenience.

Now we’re able to use C language to develop our OS. This is a huge win considering how easier it is compared to developing the OS only with Assembly Language. Next week we’ll see how we can get an output from the console. Feeling exited? yeah, me too 😉. See you next week!

Figure 7

Reference:

The Little OS Book: https://littleosbook.github.io/book.pdf

-Pasan Devin Jayawardene-

--

--

Pasan Devin Jayawardene
Pasan Devin Jayawardene

Written by Pasan Devin Jayawardene

Associate Software Engineer - RnD & AI

No responses yet