This is a little kernel written in this repository’s readme using literate programming with emacs org-mode.
To build the kernel, you need:
- Emacs with org-mode installed
- x86_64-elf-gcc (if you don’t have it, use gcc)
- GNU Make
- wget
- git
Run make
and make run
to build and run the kernel
Includes the required files such as stdint.h
for uint
, stddef.h
for size_t
, stdarg.h
for variadics arguments and stivale2.h
for bootloader structs.
#include <stdint.h>
#include <stddef.h>
#include <stivale2.h>
#include <stdarg.h>
Sets up the proper tags to get from the stivale boot protocol; Here we want to get the terminal tag from stivale which will allow us to write to the screen easily and the framebuffer tag to initialize the framebuffer.
static uint8_t stack[4096];
static struct stivale2_header_tag_terminal terminal_hdr_tag = {
.tag = {
.identifier = STIVALE2_HEADER_TAG_TERMINAL_ID,
.next = 0
},
.flags = 0
};
static struct stivale2_header_tag_framebuffer framebuffer_hdr_tag = {
.tag = {
.identifier = STIVALE2_HEADER_TAG_FRAMEBUFFER_ID,
.next = (uint64_t)&terminal_hdr_tag
},
.framebuffer_width = 0,
.framebuffer_height = 0,
.framebuffer_bpp = 0
};
__attribute__((section(".stivale2hdr"), used))
static struct stivale2_header stivale_hdr = {
.entry_point = 0,
.stack = (uintptr_t)stack + sizeof(stack),
.flags = (1 << 1) | (1 << 2),
.tags = (uintptr_t)&framebuffer_hdr_tag
};
The following function will allow us to scan for tags that we want
void *stivale2_get_tag(struct stivale2_struct *stivale2_struct, uint64_t id)
{
struct stivale2_tag *current_tag = (void *)stivale2_struct->tags;
for (;;)
{
if (!current_tag)
{
return NULL;
}
if (current_tag->identifier == id)
{
return current_tag;
}
current_tag = (void *)current_tag->next;
}
}
This is where we’ll write helper functions for our kernel, let’s start by assembly ones.
uint8_t inb(uint16_t port)
{
uint8_t data;
__asm__ volatile("inb %1, %0"
: "=a"(data)
: "d"(port));
return data;
}
void outb(uint16_t port, uint8_t data)
{
__asm__ volatile("outb %0, %1"
:
: "a"(data), "Nd"(port));
}
Then string ones
size_t strlen(char *str)
{
size_t i;
for (i = 0; str[i] != '\0'; i++);
return i;
}
char *strncat(char *dest, char *src, size_t n)
{
size_t dest_length = strlen(dest);
size_t i;
for (i = 0; i < n && src[i] != '\0'; i++)
{
dest[dest_length + i] = src[i];
}
dest[dest_length + i] = '\0';
return dest;
}
char *strcat(char *dest, char *src)
{
return strncat(dest, src, strlen(src));
}
// This function isn't perfect but it works for now :)
char *string_convert(unsigned int num, int base)
{
static char Representation[] = "0123456789ABCDEF";
static char buffer[50];
char *ptr;
ptr = &buffer[49];
*ptr = '\0';
do
{
*--ptr = Representation[num % base];
num /= base;
} while (num != 0);
return (ptr);
}
void vsprintf(char *str, char *format, va_list arg)
{
unsigned int i;
unsigned int ZERO = 0;
char *s;
int position = 0;
while (*format)
{
if (*format == '%')
{
format++;
switch (*format)
{
case 'c':
i = va_arg(arg, int);
str[position] = i;
position++;
break;
case 'd':
i = va_arg(arg, int);
if (i < ZERO)
{
i = -i;
str[position] = '-';
}
strcat(str, string_convert(i, 10));
position += strlen(string_convert(i, 10));
break;
case 'o':
i = va_arg(arg, unsigned int);
strcat(str, string_convert(i, 8));
position += strlen(string_convert(i, 8));
break;
case 's':
s = va_arg(arg, char *);
strcat(str, s);
position += strlen(s);
break;
case 'x':
i = va_arg(arg, unsigned int);
strcat(str, string_convert(i, 16));
position += strlen(string_convert(i, 16));
break;
default:
str[position] = '%';
position++;
break;
}
}
else
{
str[position] = *format;
position++;
}
format++;
}
}
In this part of the kernel, we’ll setup a COM driver to debug our kernel.
typedef enum
{
COM1 = 0x3F8,
COM2 = 0x2F8,
COM3 = 0x3E8,
COM4 = 0x2E8
} SerialPort;
static int is_transmit_empty(SerialPort port)
{
return inb(port + 5) & 0x20;
}
static int serial_received(SerialPort port)
{
return inb(port + 5) & 1;
}
void com_initialize(SerialPort port)
{
outb(port + 1, 0x00);
outb(port + 3, 0x80);
outb(port + 0, 0x03);
outb(port + 1, 0x00);
outb(port + 3, 0x03);
outb(port + 2, 0xC7);
outb(port + 4, 0x0B);
}
void com_putc(SerialPort port, char c)
{
while (is_transmit_empty(port) == 0);
outb(port, c);
}
void com_write_string(SerialPort port, char *str)
{
while (*str)
{
com_putc(port, *str++);
}
}
char com_getc(SerialPort port)
{
while (serial_received(port) == 0);
return inb(port);
}
To extend our COM driver, we can add formatting to it using printf-like formatting, let’s do that!!
void printf(char *format, ...)
{
va_list arg;
va_start(arg, format);
char message[4096] = {0};
vsprintf(message, format, arg);
com_write_string(COM1, message);
va_end(arg);
}
This is our kernel’s entry point
void _start(struct stivale2_struct *stivale2_struct) {
struct stivale2_struct_tag_terminal *term_str_tag;
term_str_tag = stivale2_get_tag(stivale2_struct, STIVALE2_STRUCT_TAG_TERMINAL_ID);
if (!term_str_tag) {
for (;;) {
__asm__ volatile("hlt");
}
}
void *term_write_ptr = (void *)term_str_tag->term_write;
void (*term_write)(const char *string, size_t length) = term_write_ptr;
term_write("Welcome to org-kernel", 21);
com_initialize(COM1);
for (;;) {
__asm__("hlt");
}
}