Developing your own operating system (Part 02)
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 esp
register 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 eax
register 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 elsewherepush 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 struct
will 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 elfall: kernel.elfkernel.elf: $(OBJECTS)
ld $(LDFLAGS) $(OBJECTS) -o kernel.elfos.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!
Resources: