Hello world!

In this first post, I mostly want to set some goals and rules, then talk about the build system and the Limine bootloader. This is all Calcite does so far:

A two dimensional gradient. The top left is black, the top right is green, the bottom left is blue, and the bottom right is cyan.

Calcite running in VirtualBox

The source for this version can be found at https://git.benjidial.net/benji/calcite/src/tag/weblog-hello-world.

Before we get into anything else, I want to set some goals and some rules for Calcite.

Goals and rules

Where it makes sense, I want to adhere to existing standards. This does not mean I will reuse existing software everywhere that I can. The whole point of writing an operating system, for me, is to learn how the things I use are implemented. As an example, when I get to the C standard library part of the user space, I want to write my own from scratch rather than port over something like the GNU C library. I do want the C standard library I create to be POSIX-compliant.

The source code of Calcite will be (for now) written only in ISO C23 and NASM assembly. At some point in the future, I might like to try writing my own Forth or Scheme interpreter, or .NET or Java runtime, and write some user space applications to use one of those.

Running Calcite on anything other than a modern, 64-bit, x86 system is a non-goal. Running binaries compiled for other operating systems is also a non-goal.

Every commit will be tested in both QEMU and VirtualBox. For now, I will only be using GCC as a C compiler, but in the future I might test versions built with Clang as well. I plan to occasionally test on real hardware (i.e. old HP laptops) as well.

Calcite will be at version 0.1.0 when I have a simple terminal emulator and shell running from binaries on the disk. Calcite will be at version 1.0.0 when it can build itself (either by porting GCC and the other tools to Calcite, or building my own compatible enough tools).

So what's in this first commit?

This commit is just the scaffolding that future versions will fit inside of (on top of? maybe scaffolding is the wrong metaphor). According to the language statistics on the repository page, this version is 42.6% C, 36.0% Makefile, 16.2% Shell, and 5.2% GDB. The GDB part is a file that I have GDB read a few commands from when I debug Calcite. I'm not sure I agree with that being counted as its own language, but I won't complain about it past this sentence.

The build system consists of a shell script named "get-dependencies.sh" that downloads the Limine bootloader, and a makefile that builds the kernel and then a disk image. The disk image has the Limine bootloader as well as the Calcite kernel.

Limine

Limine is a bootloader developed primarily by the mononymous Mintsuki. It can be found online at https://github.com/limine-bootloader/limine. Limine supports a number of boot protocols, including the Linux protocol. I first learned about Limine (or more precisely, one of its predecessors) through the OS development community, and now I use it on my PC and laptops to boot Linux and other operating systems.

Limine also supports its own protocol called the Limine boot protocol, described in "PROTOCOL.md" in the Limine repository. Limine is popular in this community, since the Limine protocol is fairly easy to use as a kernel, and it does some things for us that multiboot does not, like putting the CPU into long mode.

Calcite's kernel uses the Limine boot protocol, base revision 3. The "get-dependencies.sh" script downloads Limine version 9.3.4, and the makefile includes that in the disk image.

Calcite kernel

The kernel currently has only the following code:

#include <limine.h>

LIMINE_BASE_REVISION(3)

static volatile struct limine_framebuffer_request fb_request = {
  .id = LIMINE_FRAMEBUFFER_REQUEST,
  .revision = 0,
  .response = 0
};

[[noreturn]] static void die() {
  while (1)
    __asm__ ("hlt");
}

[[noreturn]] void kernel_entry() {

  if (fb_request.response == 0 || fb_request.response->framebuffer_count == 0)
    die();

  struct limine_framebuffer *fb = fb_request.response->framebuffers[0];

  for (uint64_t y = 0; y < fb->height; ++y)
    for (uint64_t x = 0; x < fb->width; ++x) {
      uint8_t *pixel = (uint8_t *)fb->address + y * fb->pitch + x * 4;
      pixel[0] = y * 256 / fb->height;
      pixel[1] = x * 256 / fb->width;
      pixel[2] = 0;
    }

  die();

}

In the Limine boot protocol, the kernel and the bootloader communicate via "requests" and "responses". This kernel only has a framebuffer request. Before control is passed to the kernel, if the bootloader supports framebuffer requests, the fb_request.response field is set to a pointer to the response.

The kernel first checks if there is a framebuffer response and at least one framebuffer. If there is not, it halts the CPU repeatedly. Otherwise, it fills the screen with the pattern seen in the screenshot, and then halts the CPU repeatedly.

Something to note: I am assuming that the framebuffer has a particular layout. In particular, I assume it is 32 bits per pixel, with the bottom eight bits of each pixel being a blue channel, the next eight bits a green channel, and the next eight a red channel. In the future, the framebuffer should be handled more robustly, but for now I just want to demonstrate a booting system.

What's next?

Next, I want to work on making a paging system, and have the kernel transition from the page tables provided by Limine to our own. This will allow us to map new pages of physical memory into virtual memory as needed, without having to parse and edit the tables Limine leaves us. Until then!