Welcome to the second part of Implement Your Own Operating System. (If you didn’t read the first part, please read Part 01.) This article will show you how to use C instead of assembly code as the programming language for the OS. C is a much more convenient language to use. Therefore, we would like to use C as much as possible and use assembly code only where it makes sense. However, Assembly is very good for interacting with the CPU and enables maximum control over every aspect of the code.

Let’s get started.

1. Setting Up a Stack

It’s as simple as pointing the esp register to the end of a perfectly aligned block of free memory to create a stack.
We could point esp to any random place in memory because the only memory objects are GRUB, BIOS, the OS kernel, and some memory-mapped I/O. This isn’t a good idea since we don’t know how much memory is available or if the location esp refers to is already taken.
Setting aside some uninitialized memory in the kernel’s ELF file bss section is a preferable option. The bss section should be used instead of the data section to reduce the size of the OS executable. Because GRUB recognizes ELF, it will assign any memory allocated in the bss section when launching the OS.

To declare uninitialized data, use the NASM pseudo-instruction resb:

KERNEL_STACK_SIZE equ 4096                  ; size of stack in bytes

section .bss
align 4 ; align at 4 bytes
kernel_stack: ; label points to beginning of memory
resb KERNEL_STACK_SIZE ; reserve stack for the kernel

The stack pointer is then set up by pointing esp to the end of the kernel_stack memory:

mov esp, kernel_stack + KERNEL_STACK_SIZE   ; point esp to the start of the
; stack (end of memory area)

2. Calling C Code From Assembly

The next step is to use the assembly code to invoke a C function. This article using the cdecl calling convention because GCC does. According to the cdecl calling convention, arguments to a function should be sent across the stack (on x86).
The parameters for the function should be pushed on the stack in order from right to left, with the rightmost parameter being pushed first. The return value of the function is saved in the eax register.

Consider following C code:

/* The C function */
int sum_of_three(int arg1, int arg2, int arg3)
{
return arg1 + arg2 + arg3;
}

The following code shows the call above C function from the assembly:

; The assembly code
extern sum_of_three ; the function sum_of_three is defined elsewhere

push dword 3 ; arg3
push dword 2 ; arg2
push dword 1 ; arg1
call sum_of_three ; call the function, the result will be in eax

Packing Structs

You’ll see “configuration bytes” a lot in the rest of this book, which is a collection of bits in a specified sequence. The following is a 32-bit example:

Bit:     | 31     24 | 23          8 | 7     0 |
Content: | index | address | config |

It is considerably more convenient to utilize “packed structures” instead of an unsigned integer, unsigned int, for handling such configurations:

struct example {
unsigned char config; /* bit 0 - 7 */
unsigned short address; /* bit 8 - 23 */
unsigned char index; /* bit 24 - 31 */
};

When using the struct in the previous example, there is no guarantee that the size of the struct will be exactly 32 bits - the compiler can add some padding between elements for various reasons, for example, to speed up element access or due to requirements set by the hardware and/or compiler. When using a struct to represent configuration bytes, the compiler mustn't add any padding because the struct will eventually be treated as a 32-bit unsigned integer by the hardware. The attribute packed can be used to force GCC to not add any padding:

struct example {
unsigned char config; /* bit 0 - 7 */
unsigned short address; /* bit 8 - 23 */
unsigned char index; /* bit 24 - 31 */
} __attribute__((packed));

Note that __attribute__((packed)) is not part of the C standard - it might not work with all C compilers.

3. Compiling C Code

Many flags to GCC must be used while building the C code for the OS. This is because the C code should not presume the availability of a standard library because our operating system does not have one. The GCC handbook has further information on the flags.

The flags used for compiling the C code are:

-m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector -nostartfiles
-nodefaultlibs

As always when writing C programs we recommend turning on all warnings and treat warnings as errors:

-Wall -Wextra -Werror

You can now create a function kmain in a file called kmain.c that you call from loader.s. At this point, kmain probably won’t need any arguments (but in later chapters it will).

After adding the above code snippets loader.s will look like this:

4 Build Tools

It’s also a good opportunity to put up some build tools to make compiling and testing the OS easier. We suggest make, but there are a variety of different build systems to choose from. The following is an example of a basis Makefile for the OS:

The contents of your working directory should now look like the following screenshot:

You should now be able to start the OS with the following simple command:

make run

which will compile the kernel and boot it up in Bochs (as defined in the Makefile above).

If all success, you able to see following views:

Well done! you just successfully implemented a Simple Operating System with C language. The completed OS from my Github is here. We will meet the next article Implement Your Own Operating System (Part 03).

Reference: The Little OS Book

Thank you!

--

--