Rave Bare Bones

From OSDev Wiki
Jump to navigation Jump to search

The factual accuracy of this article or section is disputed.
Please see the relevant discussion on the talk page.

This tutorial needs to explain what the code does as tutorials are not just copy paste. You can help out by editing this page to include more context to what the code does.

WAIT! Have you read Getting Started, Beginner Mistakes, and some of the related OS theory?

Difficulty level
Difficulty 1.png
Beginner
Kernel Designs
Models
Other Concepts

In this tutorial we will write a bootable kernel in the Rave programming language.

Tools needed to build the project:

  • NASM
  • Rave
  • binutils (ld)
  • QEMU (optional; for testing)


Overview

In this tutorial, we will create a simple NASM loader and a kernel capable of outputting the text we need - in this example, we will output "Hello, Rave!".

Our final project will consist of the following files:

  • boot.asm
  • kernel.rave
  • linker.ld

boot.asm

MBALIGN  equ  1 << 0            ; align loaded modules on page boundaries
MEMINFO  equ  1 << 1            ; provide memory map
MBFLAGS  equ  MBALIGN | MEMINFO ; this is the Multiboot 'flag' field
MAGIC    equ  0x1BADB002        ; 'magic number' lets bootloader find the header
CHECKSUM equ -(MAGIC + MBFLAGS) ; checksum of above, to prove we are multiboot
                                ; CHECKSUM + MAGIC + MBFLAGS should be Zero (0)
section .multiboot
align 4
	dd MAGIC
	dd MBFLAGS
	dd CHECKSUM

section .bss
align 16
stack_bottom:
resb 16384 ; 16 KiB is reserved for stack
stack_top:

section .text
global _start:function (_start.end - _start)
_start:
	mov esp, stack_top

	extern kmain
	call kmain
	cli
.hang:	hlt
	jmp .hang
.end:

Assemble that with:

nasm -felf32 boot.asm -o boot.o

kernel.rave

namespace terminal {
    (volatile) ushort* memory = 0xB8000;

    alias columns = 80;
    alias rows = 25;

    usize row = 0;
    usize column = 0;

    void clear {
        for (uint y=0; y<terminal::rows; y++) {
            for (uint x=0; x<terminal::columns; x++) terminal::memory[(y * terminal::rows) + x] = 0;
        }
    }

    void putchar(uchar symbol) {
        if (symbol == '\n') {
            terminal::column = 0;
            terminal::row += 1;
        }
        else {
            terminal::memory[(terminal::row * terminal::columns) + terminal::column] = (cast(ushort*)(&[symbol, cast(uchar)0x0F]))[0];
            terminal::column += 1;
        }

        if (terminal::column == terminal::columns) {
            terminal::column = 0;
            terminal::row += 1;

            if (terminal::row == terminal::rows) terminal::row = 0;
        }
    }

    void print(char* cstr) {
        usize i = 0;
        while (cstr[i] != '\0') { terminal::putchar(cstr[i]); i += 1; }
    }
}
 
(C) void kmain {
    terminal::clear();
    terminal::print("Hello, Rave!");

	while (true) {}
}

Сompile that with:

rave kernel.rave -o kernel.o -c --noStd --noEntry --noChecks -ffreestanding -t i686-linux

linker.ld

OUTPUT_FORMAT(elf32-i386)

/* The bootloader will look at this image and start execution at the symbol
   designated as the entry point. */
ENTRY(_start)

/* Tell where the various sections of the object files will be put in the final
   kernel image. */
SECTIONS
{
	/* It used to be universally recommended to use 1M as a start offset,
	   as it was effectively guaranteed to be available under BIOS systems.
	   However, UEFI has made things more complicated, and experimental data
	   strongly suggests that 2M is a safer place to load. In 2016, a new
	   feature was introduced to the multiboot2 spec to inform bootloaders
	   that a kernel can be loaded anywhere within a range of addresses and
	   will be able to relocate itself to run from such a loader-selected
	   address, in order to give the loader freedom in selecting a span of
	   memory which is verified to be available by the firmware, in order to
	   work around this issue. This does not use that feature, so 2M was
	   chosen as a safer option than the traditional 1M. */
	. = 2M;

	/* First put the multiboot header, as it is required to be put very early
	   in the image or the bootloader won't recognize the file format.
	   Next we'll put the .text section. */
	.text BLOCK(4K) : ALIGN(4K)
	{
		*(.multiboot)
		*(.text)
	}

	/* Read-only data. */
	.rodata BLOCK(4K) : ALIGN(4K)
	{
		*(.rodata)
	}

	/* Read-write data (initialized) */
	.data BLOCK(4K) : ALIGN(4K)
	{
		*(.data)
	}

	/* Read-write data (uninitialized) and stack */
	.bss BLOCK(4K) : ALIGN(4K)
	{
		*(COMMON)
		*(.bss)
	}

	/* The compiler may produce other sections, by default it will put them in
	   a segment with the same name. Simply add stuff here as needed. */
}

After that, you can link all of that with:

ld -melf_i386 -T linker.ld -o myos.bin boot.o kernel.o

Your kernel is now named as myos.bin. You can try to boot it using QEMU:

qemu-system-i386 -kernel myos.bin