Learn to Code via Tutorials on Repl.it!

← Back to all posts
A beginners guide to OS Dev: Part 2
h
CSharpIsGud (1026)

Last time we got an OS that just booted up and did nothing and thats great, but most operating systems do more than just nothing. And im sure you don't want to be doing the entire thing in just assembly!

So we are going to be adding C to our OS.

Using C

C requires a stack and right now we don't have one. We have to fix that

global entry

MAGIC equ 0x1BADB002 ; These tell the bootloader that we are bootable
FLAGS equ 0
SUM equ -MAGIC

section .text:
align 4 ; Align everything after this to a 4 byte boundary, which the boot header needs to be aligned to
        dd MAGIC ; dd means "define double word", a word is usually 2 bytes on most computers, so a dword is 4 bytes. You can think of a word as being a short in C and a dword being an int.
        dd FLAGS
        dd SUM

CSTACK_SIZE equ 4096 ; This is how big we want our stack to be

entry:
mov esp, cstack + CSTACK_SIZE ; The ESP register holds the current position on the stack

jmp entry ; For now we won't do anything but loop forever.

section .bss:

align 4 ; We should align our stack on a 4 byte boundary, im unsure of the consquences of not doing this but im sure you do not want to find out.
cstack:
resb CSTACK_SIZE ; resb == "reserve bytes"

Great! We have a stack and now we can call C, which we must compile separately. We don't have any standard library or protections so we need a lot of compiler flags

Here is the new compiling script

nasm -f elf32 Boot.asm

gcc -c Main.c -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector -nostartfiles -nodefaultlibs -W

ld -T link.ld -melf_i386 Boot.o Main.o -o iso/boot/kernel

genisoimage -b boot/grub/stage2_eltorito -R -boot-load-size 4 -no-emul-boot -boot-info-table -A Replit  iso > os.iso

Now lets make our C entry point and call it from the OS

int main() {
        return 0;
}

And to call it

global entry

MAGIC equ 0x1BADB002 ; These tell the bootloader that we are bootable
FLAGS equ 0
SUM equ -MAGIC

section .text:
align 4 ; Align everything after this to a 4 byte boundary, which the boot header needs to be aligned to
        dd MAGIC ; dd means "define double word", a word is usually 2 bytes on most computers, so a dword is 4 bytes. You can think of a word as being a short in C and a dword being an int.
        dd FLAGS
        dd SUM

CSTACK_SIZE equ 4096 ; This is how big we want our stack to be

entry:
mov esp, cstack + CSTACK_SIZE ; The ESP register holds the current position on the stack

extern main
call main

jmp entry ; if main returns, just do nothing. there isn't anything else to do here

section .bss:

align 4 ; We should align our stack on a 4 byte boundary, im unsure of the consquences of not doing this but im sure you do not want to find out.
cstack:
resb CSTACK_SIZE ; resb == "reserve bytes"

Now we have an OS that calls a C function

Writing text to the screen

Have you ever done console.log("Hello World") or maybe even called print a few times? Im sure you never paid much attention to what goes on far behind the scenes. Unfortunately we don't get the convenience of a print function until we make one, we are going to be using the VGA Text Mode Framebuffer as it is extremely easy to use.

// 0xB8000 is the address of the vga framebuffer
char* fb = (char*) 0xB8000;

int main() {
     // The framebuffer is a 1 dimensional "array", to get a character cell from an x and y value I did some trial and error and came up with 2 * x + 160 * y

int x = 0;
int y = 0;


// Now we get to display text!
// First you write the letter
// Then you write the color attribute you want to write it in, we will just use white on black for now

// We can do this better and more efficiently later
int position = 2 * x + 160 * y;

fb[position] = 'H';
fb[position + 1] = 0xF0;
x++;
position = 2 * x + 160 * y;

fb[position] = 'i';
fb[position + 1] = 0xF0;
}

Once compiled and ran this should output Hi to the screen!
Unfortunately we can't do all of an operating system with just C.

Comments
hotnewtop
AmazingMech2418 (1082)

I'm trying to convert my OS to C instead of all Assembly now so I can make the second part of my tutorial and the second version of my OS, but for some reason, string literals of over 63 characters are causing boot failures. Do you have any suggestions?

CSharpIsGud (1026)

@AmazingMech2418 Use char str[] = "..."; instead, something about where literals are put in memory mess with the OS

AmazingMech2418 (1082)

@CSharpIsGud I used that and it still caused the issue. For some reason, it's only for 64 or more characters though. If I use 63 characters, it's fine, but I need 80 characters for this specific string.

AmazingMech2418 (1082)

@CSharpIsGud Also, if I push a variable from Assembly to C, it seems to work, but it doesn't work if I define it in C.

CSharpIsGud (1026)

@AmazingMech2418 does it happen if you make two literals like one 32 chars and another 35 chars long?

AmazingMech2418 (1082)

@CSharpIsGud That does not cause the issue.

fuzzyastrocat (1864)

Awesome! Combining this tutorial with @AmazingMech2418's helped me to understand how some of this stuff works. Great job!

fuzzyastrocat (1864)

Whenever I use pointers (or more specifically, strings) I get this error:
Could not open option rom 'linuxboot_dma.bin': No such file or directory

Do you know what would cause that? (My project is a little different than yours, you can look at it here: https://repl.it/@fuzzyastrocat/OS)
Even just doing char* mystr = "A string"; in main() causes this error.

fuzzyastrocat (1864)

@CSharpIsGud I've actually fixed the issue — instead of

print("Hello, world!");

I do

char chrs[] = "Hello, world!";
print(chrs);

Not sure why this happens, since I thought pointers and arrays were essentially interchangeable, but it works so yay!

CSharpIsGud (1026)

@fuzzyastrocat string literals get stored in a section like .rodata
while assigning it to an array makes an array in memory and copies the characters to it.

fuzzyastrocat (1864)

@CSharpIsGud Oh, interesting — so there must be something strange with .rodata then, using it tries to find linuxboot_dma.bin

CodeLongAndPros (1624)

One thing:
On Bochs, the byte 0xF0 sets the background color to 15, fg to 0, and sets the blink byte. The byte 0x70 disables blink,but I can't get it to be white on black. Thanks!

CodeLongAndPros (1624)

@CSharpIsGud I’ll try that first chance I get.

CodeLongAndPros (1624)

@CSharpIsGud Perhaps I have a terrible toolchain, but 0x0F gave a blink.

AmazingMech2418 (1082)

@CodeLongAndPros @CSharpIsGud For the colors, the binary representation helps the most in my opinion. For 0xF0, it is the same as 1 111 0000. However, for white on black, you would need 0 000 1111 which is 0x0F. The first bit is the blink byte. The next three are the background. And, the last four are the foreground. When it comes to converting it to hexadecimal, the second digit is always the foreground while the first is the background, but it only ranges from 0 to 7 since there is also the blink bit.

CodeLongAndPros (1624)

Thank you for this. This was the straw that broke the camel's back with my kprint(), the camel being bugs.

Highwayman (1481)

mov esp, cstack + CSTACK_SIZE

Could you try and explain this line? I don’t really get it sorry I don’t really do assembly I’ve been holding out until you got to the point where c is a thing lol.

Also why do you add 0xF0 after each character?

CSharpIsGud (1026)

@Highwayman it moves cstack + CSTACK_SIZE into the esp register
think of it like esp = cstack +CSTACK_SIZE, except cstack in this case is an address to where the stack should be. 0xF0 is the color byte, which you write after every color. there are only 15 colors, 0 black to F white
so a white foreground on black background is 0xF0

Highwayman (1481)

@CSharpIsGud Sry should have been more clear, I know that, but my questions is if the esp "holds the current position on the stack", then isn’t it just going to fly right off the end of the stack when you try to use it because you put it at the end of the stack’s reserves bytes?

CodeLongAndPros (1624)

@Highwayman On x86 the stack grows downwards. So 0xFFFF would be the first byte, if you start at 0xFFFF.

Highwayman (1481)

@CodeLongAndPros oh oops I can’t believe I forgot such a basic thing XD thank you! :)

Highwayman (1481)

UwU the second part! Yay!