Let’s make your own operating system (#week 2)

Nimantha Gayan
7 min readJul 23, 2021

--

In the 1st week we helped you to set up your development environment and booting the OS. In this 2nd week we will show you how to use C instead of assembly code as the programming language for the OS.

Implement with C

This part I will show you how to use C instead of assembly code as the programming language for the OS. Assembly is very good for interacting with the CPU and enables maximum control over every aspect of the code. I would like to use C as much as possible and use assembly code only where it makes sense.

Setting Up a Stack

Since all non-trivial (not lightweight) C programs use a stack, and setting up a stack is not harder than to make the esp register point to the end of an area of free memory. So far, in this development process, the only things in memory are GRUB, BIOS, the OS kernel, and some memory mapped I/Os. This is not a good thing to do; because, we don’t know how much memory is available or if the esp pointed memory area is used by something else.

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 file.

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

And then, we need to setup the stack pointer by pointing esp to the end of the kernel_stack memory. In order to do that, you need to add the following statement inside the loader: block you your loader.s file

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

Calling C Code from Assembly

The next step is to call a C function from assembly code. There are many different conventions for how to call C code from assembly code . This series uses the cdecl calling convention, since that is the one used by GCC. The cdecl calling convention states that arguments to a function should be passed via the stack (on x86). The arguments of the function should be pushed on the stack in a right-to-left order, that is, you push the rightmost argument first. The return value of the function is placed in the eax register.

The following code shows an example,

is, you push the rightmost argument first.

The C function

int sum_of_three(int arg1, int arg2, int arg3)
{
return arg1 + arg2 + arg3;
}

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

You need to write that C function in a text file and save that as kmain.c and add that assembly code in to loader.s file.

Now modified loader.s file should look like this,

global loader                   ; the entry symbol for ELF
extern sum_of_three

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)

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

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

loader: ; the loader label (defined as entry point in linker script)
mov eax, 0xCAFEBABE ; place the number 0xCAFEBABE in the register eax
mov esp, kernel_stack + KERNEL_STACK_SIZE ; point esp to the start of the
; stack (end of memory area)

push dword 3
push dword 2
push dword 1
call sum_of_three


loader: ; the loader label (defined as entry point in linker script)
mov eax, 0xCAFEBABE ; place the number 0xCAFEBABE in the register eax
.loop:
jmp .loop ; loop forever

Packing Structs

In the rest of this series, you will often come across “configuration bytes” that are a collection of bits in a very specific order. Below follows an example with 32 bits:

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

Instead of using an unsigned integer, unsigned int, for handling such configurations, it is much more convenient to use “packed structures”:

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, it is very important that the compiler does not 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.

Compiling C Code

When compiling the C code for the OS, a lot of flags to GCC need to be used. This is because the C code should not assume the presence of a standard library, since there is no standard library available for our OS. For more information about the flags, see the GCC manual.

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 series it will).

Build Tools

Now is also probably a good time to set up some build tools to make it easier to compile and test-run the OS. We recommend using make, but there are plenty of other build systems available. A simple Makefile for the OS could look like the following example:

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

you put your OS name in place of windOS.

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

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

in final step, You should now be able to start the OS with the simple command make run, which will compile the kernel and boot it up in Bochs.

Now you can a window like this

then your terminal and type ‘c’ like this,

now you can see your OS is successfully boot,

After quitting Bochs, display the log produced by Boch:

cat bochslog.txt

You should now see the contents of the registers of the CPU simulated by Bochs somewhere in the output. If you find EAX=00000006 in the output then your OS has successfully booted!

This is the end of the second article of the ‘How to build an operating system’ article series. Hope you get the best out of this. In next article we will be discussing about ‘playing with outputs’.

--

--

Nimantha Gayan
Nimantha Gayan

Written by Nimantha Gayan

Software Engineering , University Of Kelaniya

No responses yet