Build your own Operating System #2

Gayan Malinda
6 min readJul 21, 2021

--

Implementation with C Programming Language

Hello everyone!

This is the second article of “ Build your own Operating System “ article series. In this article series, I guide you to develop your own OS. It be a simple. But it’s your own one. After this article series, you be a owner of your own operating system.

In my first article, I introduced what is an OS and how to set up the development environment and booting a primitive operating system. 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. click below linking box to follow it.

This article will give assistance to you in using the C programming language instead of assembly code as the programming language for the OS. Assembly language is good for interacting with the CPU as there is a strong correspondence between the instructions in the language and the machine code instructions. Other than that, assembly enables maximum control over every aspect of the code. However, C is a much more convenient language than assembly to be used by us as it is more towards human language and it has direct access to the memory. I previously mentioned this article assists you in Implementation with C.

Setting Up a Stack

One prerequisite for using C programming is a stack, as all non-trivial C programs use a stack. Stack is a specially allocated area in computer memory (Random Access Memory) to store temporary variables created by functions. Almost all pragmatic C programs will require this space to store data in their functions.

Memory allocation in RAM

We need to allocate this space for stack in memory by make the esp register point to the end of an area of free memory that is correctly aligned. (As for reminder, so far we only have BIOS, GRUB, the OS kernel and some memory- mapped I/O in the memory) When we pointing we need to be careful and cannot do it randomly because we have no idea about how much memory is available or if the area esp would point to a memory location that is used by something else.

A better solution is to set aside some uninitialized memory in the kernel’s ELF file’s bss section. GRUB will allocate any memory reserved in the bss section when loading the OS because it understands ELF. And also with this solution it will reduce the size of OS executable.

The NASM pseudo-instruction resb can be used to declare uninitialized data. After adding these NASM pseudo-instructions to the loader.s file you will look like this.

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.

Calling C Code From Assembly

Then as the next step let’s call a C function from assembly code. Here, the cdecl calling convention is used for calling C code from assembly code, as it is the convention used by GCC. The cdecl calling convention states that arguments to a function should be passed via the stack (on x86). It is the arguments of the function should be pushed on the stack in a right-to-left order. The return value of the function is placed in the eax register. The following code shows an example for cdecl calling convention.

Save kmain.c file in your working directory with this code.

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

According to the cdecl calling convention, arguments to functions should be passed through the stack.

To do this place the following code after the esp instruction in the loader label of the loader.s.

Compiling C Code

Many flags to GCC must be used while compiling the C code for the OS. This is because the C code should not assume the presence of a standard library because our operating system does not have one.

What is a Flag?

The C preprocessor, which is run on each C source file before real compilation, is controlled by these parameters.

The flags used for compiling the C code are:

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

Below I provide some information For your further understanding of flags.

-Wall -Wextra –Werror ; turning on all warnings and treat warnings as errors-nostdlib ; Do not use the standard system startup files or libraries when linking.-nostartfiles ; Do not use the standard system startup files when linking. The standard system libraries are used normally, unless -nostdlib, -nolibc, or -nodefaultlibs is used.-nodefaultlibs ; Do not use the standard system libraries when linking. Only the libraries you specify are passed to the linker, and options specifying linkage of the system libraries, such as -static-libgcc or -shared-libgcc, are ignored. The standard startup files are used normally, unless -nostartfiles is used.-fno-stack-protector ; there will be a little more space allocated on the stack and a little more overhead on entry to and return from a function while the code sets up the checks and then actually checks whether you’ve overwritten the stack while in the function.-nolibc ; Do not use the C library or system libraries tightly coupled with it when linking.

Build Tools

Now we will set up some build tools to make it easier to compile and test-run the OS.

We will use make as our build system, but there are plenty of other build systems available. Make is a build automation tool that automatically builds executable programs and libraries from source code by reading files called Makefiles which specify how to derive the target program. So, first let’s create a Makefile in our working directory and save the following information in it.

The make utility can be useful if you wish to run or update a task when particular files are updated. The Makefile (or makefile) file is required by the make utility. It defines a series of tasks to be performed. Make is a command that allows you to compile a program from source code. Make is used by most open-source projects to generate a final executable binary, which can subsequently be installed with install make. (which we have already installed). If not we have to recompile all files that we have updated.

At the end of all these steps the contents of your working directory should look as follows.

Now you have almost reached the end of your process in implementing your OS with C. You will be able to start your developed operating system with the simple command “make run” on your terminal. “make run” command will compile the kernel and boot it up in Bochs.

Then run continue command in the interactive start menu.

Quit Bochs and display the log generated by Bochs with the cat bochslog.txt command.

You should see number 6 in the eax register. That is because the function has returned number 6 (sum of the arguments we provided).

Congratulations! Now you have finished implementing your own OS with C programming. In the next article, you will be presented with the way of displaying text on the console and writing data to the serial port. Thank you so much for reading!

References:

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

--

--

Gayan Malinda

Software Engineering Undergraduate - University of Kelaniya, Sri Lanka