Developing your own operating system (Part 02)

Sheran Randika
5 min readOct 12, 2022

--

Hello everyone! Welcome to Develop Your Own Operating System Part 2. (If you haven’t already, please read Part 01.) This article will clearly show how to use C instead of assembly code as the programming language for the OS. The language C is much easier to work with. Therefore, we’d like to use C as much as possible and assembly code only when necessary.

Setting Up a Stack

To create a stack, all that needs to be done is point the espregister at the end of a perfectly aligned block of free memory. Because GRUB, BIOS, the OS kernel, and some memory-mapped I/O are the only memory objects, we could point esp to any random location in memory. We don’t know how much memory is available or whether the location esp refers to is already taken, so this isn’t a good idea.It is preferable to reserve some uninitialized memory in the kernel’s ELF file’s bss section. To make the OS executable smaller, the bss section should be utilized rather than the data section. Since GRUB understands ELF, it will allot any memory set aside in the bss section when the OS is launched.

The NASM pseudo-instruction resb can be used to declare uninitialized data:

KERNEL_STACK_SIZE equ 4096           ; size of stack in bytessection .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)

Calling C Code from Assembly

The next step is to execute assembly code to call a C function. The conventions for calling C code from assembly code vary widely. Since GCC uses the cdecl calling convention, this book also does. According to the cdecl calling convention, arguments should be passed to a function via the stack (on x86). The function’s arguments should be added to the stack from right to left, starting with the argument on the right. The eaxregister receives the function’s return value and stores it there. An illustration is shown in the code below:

Consider following C code:

/* The C function */
int kmain(int arg1, int arg2, int arg3)
{
return arg1 + arg2 + arg3;
}
; The assembly code
extern kmain ; the function kmain is defined elsewhere
push dword 3 ; arg3
push dword 2 ; arg2
push dword 1 ; arg1
call kmain ; call the function, the result will be in eax

Packing Structs

In the rest of this article, you’ll see the term “configuration bytes,” which is a collection of bits in a specific sequence. Listed below is a 32-bit illustration:

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

It is far more convenient to handle such configurations using “packed structures” rather than an unsigned integer, unsigned int:

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

There is no guarantee when using the struct in the previous example that the size of the structwill be exactly 32 bits; the compiler may add padding between elements for a variety of reasons, such as to speed up element access or due to specifications set by the hardware and/or compiler. Because the hardware will eventually treat the struct as a 32-bit unsigned integer, the compiler must avoid adding padding when using a struct to represent configuration bytes. To prevent GCC from adding any padding, use the attribute packed:

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

A large number of flags to GCC must be used when compiling the C code for the OS 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

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

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.

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.elfkernel.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 the following figure:

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

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.

Quit the Bochs and use the command cat bochslog.txt to display the log it generated. If you can find the summation EAX = 000000006 (the sum of the arguments we provided), you are successful!

So, this is the Second part of the Developing of my simple operating system. I hope to see you again in the next part!

Thank you!

--

--