Let’s build OS- #Week 02

Kavindu Abeywickrama
5 min readOct 12, 2022

--

Implement Operating system with C

Welcome back to the second episode of my operating system article series. I covered how to use Ubuntu to create a basic operating system in my first article. This article will describe the implement Operating system with c.

If you still haven’t gone through the previous article, please refer to the below mentioned link.

We have been developing our operating system in Assembly language up until this point. However, we need to use a convenient programming language like C when we start delving into the coding parts. This article will describe how to enhance our basic operating system using both the C and Assembly languages.

Why C language over Assembly?

Assembly offers complete control over every component of the code and is very effective for dealing with the CPU. C, on the other hand, is far more practical to use, at least for the authors. Therefore, I would choose to utilize C whenever possible and assembly code only when essential.

Setting Up a Stack

One prerequisite for using C is a stack since all non-trivial C programs use a stack. Setting up a stack is not harder than making the esp register point to the end of an area of free memory (remember that the stack grows towards lower addresses on the x86) that is correctly aligned (alignment on 4 bytes is recommended from a performance perspective).

Reserving a piece of uninitialized memory in the bss section in the ELF file of the kernel will be a solution. And also, this will reduce the OS executable size.

Add this section to loader.s the file We made earlier in week 1.

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

Then add the following code to set up the stack pointer: This is done 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)

Calling C Code from Assembly

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.

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

To achieve the task, put the following code after the esp instruction in the loader.s file.

; The assembly code
external 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

After the above changes, The final “loader.s” file would be like this.

global loader                   ; the entry symbol for ELF

MAGIC_NUMBER equ 0x1BADB002 ; define the magic number constant
FLAGS equ 0x0 ; multiboot flags
CHECKSUM equ -MAGIC_NUMBER ; calculate the checksum
; (magic number + checksum + flags should equal 0)
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

section .text: ; start of the text (code) section
align 4 ; the code must be 4 byte aligned
dd MAGIC_NUMBER ; write the magic number to the machine code,
dd FLAGS ; the flags,
dd CHECKSUM ; and the checksum

loader: ; the loader label (defined as entry point in linker script)
mov esp, kernel_stack + KERNEL_STACK_SIZE ; point esp to the start of the
;stack (end of memory area)
; 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

.loop:
jmp .loop ; loop forever

Compiling C code

When compiling the C code for the OS, a lot of flags to GCC need to be used because otherwise, the C code might assume the presence of a standard library.

The flags used for compiling the C code are

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

Keep in mind that it is highly recommended to turn on all warnings and treat warnings as errors when writing C programs.

Then we can call kmain function from loader.s by putting it in the file named kmain.c"at this stage, it will not take any arguments.

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.

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

Here’s a simple Makefile for our OS:

OBJECTS = loader.o kmain.o
CC = gcc
CFLAGS = -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector \
-nostartfiles -nodefaultlibs -Wall -Wextra -Werror -c
LDFLAGS = -T link.ld -melf_i386
AS = nasm
ASFLAGS = -f elf

all: kernel.elf

kernel.elf: $(OBJECTS)
ld $(LDFLAGS) $(OBJECTS) -o kernel.elf

os.iso: kernel.elf
cp kernel.elf iso/boot/kernel.elf
genisoimage -R \
-b boot/grub/stage2_eltorito \
-no-emul-boot \
-boot-load-size 4 \
-A os \
-input-charset utf8 \
-quiet \
-boot-info-table \
-o os.iso \
iso

run: os.iso
bochs -f bochsrc.txt -q

%.o: %.c
$(CC) $(CFLAGS) $< -o $@

%.o: %.s
$(AS) $(ASFLAGS) $< -o $@

clean:
rm -rf *.o kernel.elf os.iso

The contents of your working directory should now look like :

|-- bochsrc.txt
|-- iso
| |-- boot
| |-- grub
| |-- menu.lst
| |-- stage2_eltorito
|-- kmain.c
|-- loader.s
|-- Makefile

Now you should be able to start the OS with the simple command make run . This will compile the kernel and boot it up in Bochs. Then you will get the following output.

Finally, quit the Bochs and then use the command cat bochslog.txt to view the log it generated. You will be successful if you can get the sum, EAX = 000000006 (the sum of the arguments we provided)..!

Thank you for reading!

I hope to see you again in the next article!

-Kavindu Abeywickrama

--

--