aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorClyne Sullivan <clyne@bitgloo.com>2024-09-27 06:02:49 -0400
committerClyne Sullivan <clyne@bitgloo.com>2024-09-27 06:02:49 -0400
commitd43d7caf15a01dd9c2ef1d9e975df3ef7c4e9204 (patch)
treeaaf1f0f3a18b6f7b3fc25022e06941a9fb4fa612
wip: initial commit
-rw-r--r--.gitignore5
-rw-r--r--Makefile45
-rw-r--r--boot.s127
-rw-r--r--gdt.cpp81
-rw-r--r--gdt.hpp7
-rw-r--r--idt.cpp107
-rw-r--r--idt.hpp21
-rw-r--r--iso/boot/grub/grub.cfg4
-rw-r--r--kernel.cpp55
-rw-r--r--link.ld66
-rw-r--r--memory.cpp87
-rw-r--r--memory.hpp7
-rw-r--r--multiboot.cpp41
-rw-r--r--multiboot.hpp7
-rw-r--r--pic.cpp63
-rw-r--r--pic.hpp12
-rw-r--r--pit.cpp39
-rw-r--r--pit.hpp10
-rw-r--r--portio.hpp24
-rw-r--r--tasking.cpp60
-rw-r--r--tasking.hpp12
-rw-r--r--textoutput.hpp44
-rw-r--r--vgaterminal.cpp54
-rw-r--r--vgaterminal.hpp51
24 files changed, 1029 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..540202a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+*.bin
+*.iso
+*.o
+*.sw*
+iso/boot/*.bin
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..17adbac
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,45 @@
+ASFLAGS := --32
+CXXFLAGS := -m32 -ggdb -g3 -O0 -fno-pic -ffreestanding -fno-rtti -fno-exceptions -std=c++23
+LDFLAGS := -m32 -static -T link.ld -ffreestanding -nostdlib
+
+ASFILES := boot.s
+CXXFILES := gdt.cpp \
+ idt.cpp \
+ memory.cpp \
+ multiboot.cpp \
+ pic.cpp \
+ pit.cpp \
+ tasking.cpp \
+ vgaterminal.cpp \
+ kernel.cpp
+
+OBJS := $(subst .s,.o,$(ASFILES)) \
+ $(subst .cpp,.o,$(CXXFILES))
+
+all: myos.iso
+
+myos.iso: myos.bin iso/boot/grub/grub.cfg
+ @echo " ISO " $@
+ @cp myos.bin iso/boot/
+ @grub-mkrescue -o myos.iso iso/
+
+myos.bin: $(OBJS) link.ld
+ @echo " LD " $@
+ @g++ $(LDFLAGS) -o $@ $(OBJS)
+
+%.o: %.s
+ @echo " AS " $<
+ @as $(ASFLAGS) -c $< -o $@
+
+%.o: %.cpp
+ @echo " CXX " $<
+ @g++ $(CXXFLAGS) -c $< -o $@
+
+clean:
+ @echo " CLEAN"
+ @rm -f $(OBJS) myos.bin myos.iso
+
+run: myos.iso
+ @echo " QEMU"
+ @qemu-system-i386 -cdrom $< -monitor stdio -no-reboot -s -S #-d int
+
diff --git a/boot.s b/boot.s
new file mode 100644
index 0000000..7908d3e
--- /dev/null
+++ b/boot.s
@@ -0,0 +1,127 @@
+/* Declare constants for the multiboot header. */
+.set MAGIC, 0xE85250D6
+.set FLAGS, 0x0
+.set LENGTH, 16
+.set CHECKSUM, -(MAGIC + FLAGS + LENGTH)
+
+.section .multiboot2
+.align 8
+.int MAGIC
+.int FLAGS
+.int LENGTH
+.int CHECKSUM
+
+/* info request */
+.align 8
+.hword 1, 0
+.int 12
+.int 4
+
+/* end tag */
+.align 8
+.hword 0, 0
+.int 8
+
+/*
+The multiboot standard does not define the value of the stack pointer register
+(esp) and it is up to the kernel to provide a stack. This allocates room for a
+small stack by creating a symbol at the bottom of it, then allocating 16384
+bytes for it, and finally creating a symbol at the top. The stack grows
+downwards on x86. The stack is in its own section so it can be marked nobits,
+which means the kernel file is smaller because it does not contain an
+uninitialized stack. The stack on x86 must be 16-byte aligned according to the
+System V ABI standard and de-facto extensions. The compiler will assume the
+stack is properly aligned and failure to align the stack will result in
+undefined behavior.
+*/
+.section .bss
+.align 16
+stack_bottom:
+.skip 16384 # 16 KiB
+stack_top:
+
+/*
+The linker script specifies _start as the entry point to the kernel and the
+bootloader will jump to this position once the kernel has been loaded. It
+doesn't make sense to return from this function as the bootloader is gone.
+*/
+.section .text
+.global _start
+.type _start, @function
+_start:
+ /*
+ The bootloader has loaded us into 32-bit protected mode on a x86
+ machine. Interrupts are disabled. Paging is disabled. The processor
+ state is as defined in the multiboot standard. The kernel has full
+ control of the CPU. The kernel can only make use of hardware features
+ and any code it provides as part of itself. There's no printf
+ function, unless the kernel provides its own <stdio.h> header and a
+ printf implementation. There are no security restrictions, no
+ safeguards, no debugging mechanisms, only what the kernel provides
+ itself. It has absolute and complete power over the
+ machine.
+ */
+ mov %eax, multiboot_magic
+ mov %ebx, multiboot_ptr
+
+ /*
+ To set up a stack, we set the esp register to point to the top of the
+ stack (as it grows downwards on x86 systems). This is necessarily done
+ in assembly as languages such as C cannot function without a stack.
+ */
+ mov $stack_top, %esp
+
+ /*
+ This is a good place to initialize crucial processor state before the
+ high-level kernel is entered. It's best to minimize the early
+ environment where crucial features are offline. Note that the
+ processor is not fully initialized yet: Features such as floating
+ point instructions and instruction set extensions are not initialized
+ yet. The GDT should be loaded here. Paging should be enabled here.
+ C++ features such as global constructors and exceptions will require
+ runtime support to work as well.
+ */
+ mov $__init_array_start, %eax
+.again:
+ cmp $__init_array_end, %eax
+ je .next
+ push %eax
+ call *(%eax)
+ pop %eax
+ add $0x4, %eax
+ jmp .again
+
+.next:
+
+ /*
+ Enter the high-level kernel. The ABI requires the stack is 16-byte
+ aligned at the time of the call instruction (which afterwards pushes
+ the return pointer of size 4 bytes). The stack was originally 16-byte
+ aligned above and we've pushed a multiple of 16 bytes to the
+ stack since (pushed 0 bytes so far), so the alignment has thus been
+ preserved and the call is well defined.
+ */
+ call kernel_main
+
+ /*
+ If the system has nothing more to do, put the computer into an
+ infinite loop. To do that:
+ 1) Disable interrupts with cli (clear interrupt enable in eflags).
+ They are already disabled by the bootloader, so this is not needed.
+ Mind that you might later enable interrupts and return from
+ kernel_main (which is sort of nonsensical to do).
+ 2) Wait for the next interrupt to arrive with hlt (halt instruction).
+ Since they are disabled, this will lock up the computer.
+ 3) Jump to the hlt instruction if it ever wakes up due to a
+ non-maskable interrupt occurring or due to system management mode.
+ */
+ cli
+1: hlt
+ jmp 1b
+
+/*
+Set the size of the _start symbol to the current location '.' minus its start.
+This is useful when debugging or when you implement call tracing.
+*/
+.size _start, . - _start
+
diff --git a/gdt.cpp b/gdt.cpp
new file mode 100644
index 0000000..27b9f42
--- /dev/null
+++ b/gdt.cpp
@@ -0,0 +1,81 @@
+#include <array>
+#include <cstdint>
+
+struct gdt_entry_bits {
+ std::uint32_t limit_low : 16;
+ std::uint32_t base_low : 24;
+ std::uint32_t accessed : 1;
+ std::uint32_t read_write : 1; // readable for code, writable for data
+ std::uint32_t conforming_expand_down : 1; // conforming for code, expand down for data
+ std::uint32_t code : 1; // 1 for code, 0 for data
+ std::uint32_t code_data_segment : 1; // should be 1 for everything but TSS and LDT
+ std::uint32_t DPL : 2; // privilege level
+ std::uint32_t present : 1;
+ std::uint32_t limit_high : 4;
+ std::uint32_t available : 1; // only used in software; has no effect on hardware
+ std::uint32_t long_mode : 1;
+ std::uint32_t big : 1; // 32-bit opcodes for code, uint32_t stack for data
+ std::uint32_t gran : 1; // 1 to use 4k page addressing, 0 for byte addressing
+ std::uint32_t base_high : 8;
+} __attribute__((packed));
+
+constinit static const std::array<gdt_entry_bits, 3> gdt {{
+ {},
+ /* kernel_code = */ {
+ .limit_low = 0xFFFF,
+ .base_low = 0x0000,
+ .accessed = 0,
+ .read_write = 1,
+ .conforming_expand_down = 0,
+ .code = 1,
+ .code_data_segment = 1,
+ .DPL = 0,
+ .present = 1,
+ .limit_high = 0xF,
+ .available = 0,
+ .long_mode = 0,
+ .big = 1,
+ .gran = 1,
+ .base_high = 0x00
+ },
+ /* kernel_data = */ {
+ .limit_low = 0xFFFF,
+ .base_low = 0x0000,
+ .accessed = 0,
+ .read_write = 1,
+ .conforming_expand_down = 0,
+ .code = 0,
+ .code_data_segment = 1,
+ .DPL = 0,
+ .present = 1,
+ .limit_high = 0xF,
+ .available = 0,
+ .long_mode = 0,
+ .big = 1,
+ .gran = 1,
+ .base_high = 0x00
+ }
+}};
+
+void gdt_initialize()
+{
+ auto gdtr = reinterpret_cast<std::uint64_t>(gdt.data());
+ gdtr <<= 16;
+ gdtr |= gdt.size() * sizeof(gdt[0]);
+
+ asm volatile(R"(
+ lgdt %0
+ pushl $0x8
+ push $.setcs
+ ljmp *(%%esp)
+ .setcs:
+ add $8, %%esp
+ mov $0x10, %%eax
+ mov %%eax, %%ds
+ mov %%eax, %%es
+ mov %%eax, %%fs
+ mov %%eax, %%gs
+ mov %%eax, %%ss
+ )" :: "m"(gdtr));
+}
+
diff --git a/gdt.hpp b/gdt.hpp
new file mode 100644
index 0000000..0d7a9b6
--- /dev/null
+++ b/gdt.hpp
@@ -0,0 +1,7 @@
+#ifndef GDT_HPP
+#define GDT_HPP
+
+void gdt_initialize();
+
+#endif // GDT_HPP
+
diff --git a/idt.cpp b/idt.cpp
new file mode 100644
index 0000000..cacac97
--- /dev/null
+++ b/idt.cpp
@@ -0,0 +1,107 @@
+#include "idt.hpp"
+#include "portio.hpp"
+#include "textoutput.hpp"
+
+#include <array>
+#include <cstdint>
+#include <utility>
+
+extern TextOutput& term;
+
+static constexpr std::uint8_t TaskGate = 0x5;
+static constexpr std::uint8_t IntrGate16 = 0x6;
+static constexpr std::uint8_t TrapGate16 = 0x7;
+static constexpr std::uint8_t IntrGate32 = 0xE;
+static constexpr std::uint8_t TrapGate32 = 0xF;
+
+struct idt_entry_bits {
+ std::uint32_t offset_low : 16;
+ std::uint32_t segment_selector : 16;
+ std::uint32_t rsvd : 8;
+ std::uint32_t gate_type : 4;
+ std::uint32_t rsvd2 : 1;
+ std::uint32_t dpl : 2;
+ std::uint32_t present : 1;
+ std::uint32_t offset_high : 16;
+} __attribute__((packed));
+
+static std::array<Callback, 48> callbacks;
+
+extern "C"
+void interruptGeneralHandler(Registers regs)
+{
+ const auto& inum = regs.inum;
+
+ if (inum >= 32) {
+ if (inum >= 40)
+ outb(0xA0, 0x20);
+
+ outb(0x20, 0x20);
+ }
+
+ if (inum < callbacks.size()) {
+ if (auto cb = callbacks[inum]; cb)
+ cb(regs);
+ }
+}
+
+template<std::size_t N>
+struct StubEntry
+{
+ static constexpr bool HasError = N == 8 || (N >= 10 && N <= 14) || N == 17 || N == 30;
+
+ __attribute__((naked))
+ static void stub() {
+ if constexpr (!HasError)
+ asm volatile("push $0x0");
+
+ asm volatile(R"(
+ pusha
+ push %0
+ cld
+ call interruptGeneralHandler
+ pop %%eax
+ popa
+ add $0x4, %%esp
+ iret
+ )" :: "i"(N));
+ }
+
+ static constexpr std::uint32_t segment(std::uint16_t gdt_idx, bool useLdt, std::uint16_t rpl) {
+ return gdt_idx | (useLdt ? 0x4 : 0x0) | (rpl & 0x3);
+ }
+
+ idt_entry_bits entry = {
+ .offset_low = (uint32_t)stub & 0xFFFF,
+ .segment_selector = segment(0x8, false, 0),
+ .gate_type = IntrGate32,
+ .dpl = 0,
+ .present = 1,
+ .offset_high = (uint32_t)stub >> 16
+ };
+
+ operator idt_entry_bits() const noexcept {
+ return entry;
+ }
+};
+
+static auto idt =
+ []<std::size_t... ints>(std::index_sequence<ints...>) {
+ return std::array<idt_entry_bits, 256> { StubEntry<ints>()... };
+ }(std::make_index_sequence<48>{});
+
+void idt_initialize()
+{
+ auto idtr = reinterpret_cast<std::uint64_t>(idt.data());
+ idtr <<= 16;
+ idtr |= idt.size() * sizeof(idt[0]);
+
+ asm volatile("lidt %0" :: "m"(idtr));
+}
+
+void idt_register_callback(std::size_t num, Callback cb)
+{
+ if (num < callbacks.size())
+ callbacks[num] = cb;
+}
+
diff --git a/idt.hpp b/idt.hpp
new file mode 100644
index 0000000..c9092dd
--- /dev/null
+++ b/idt.hpp
@@ -0,0 +1,21 @@
+#ifndef IDT_HPP
+#define IDT_HPP
+
+#include <cstddef>
+#include <cstdint>
+
+struct Registers
+{
+ std::uint32_t inum;
+ std::uint32_t edi, esi, ebp, esp, ebx, edx, ecx, eax;
+ std::uint32_t error;
+ std::uint32_t eip, cs, eflags;
+} __attribute__((packed));
+
+using Callback = void (*)(const Registers&);
+
+void idt_initialize();
+void idt_register_callback(std::size_t num, Callback cb);
+
+#endif // IDT_HPP
+
diff --git a/iso/boot/grub/grub.cfg b/iso/boot/grub/grub.cfg
new file mode 100644
index 0000000..24aafbd
--- /dev/null
+++ b/iso/boot/grub/grub.cfg
@@ -0,0 +1,4 @@
+menuentry "myos" {
+ multiboot2 /boot/myos.bin
+}
+
diff --git a/kernel.cpp b/kernel.cpp
new file mode 100644
index 0000000..41c3a61
--- /dev/null
+++ b/kernel.cpp
@@ -0,0 +1,55 @@
+#include "gdt.hpp"
+#include "idt.hpp"
+#include "memory.hpp"
+#include "multiboot.hpp"
+#include "pic.hpp"
+#include "pit.hpp"
+#include "tasking.hpp"
+#include "vgaterminal.hpp"
+
+static VGATerminal vga;
+TextOutput& term = vga;
+
+extern "C"
+void kernel_main(void)
+{
+ term.write("Clyne's kernel, v2024\n\n");
+
+ if (!multiboot_initialize())
+ for (;;);
+
+ idt_register_callback(14, [](const Registers& regs) {
+ term.write("Page fault! eip=");
+ term.write(regs.eip);
+ term.write('\n');
+ for (;;);
+ });
+
+ memory_initialize();
+ gdt_initialize();
+ pic_initialize();
+ idt_initialize();
+ pit_initialize(50);
+ asm volatile("sti");
+ tasking_initialize();
+ term.write("Tasking enabled.\n");
+
+ tasking_spawn([] {
+ for (;;)
+ term.write('B');
+ }, 256);
+
+ for (;;)
+ term.write('A');
+}
+
+extern "C"
+void memmove(char* dst, char* src, size_t sz) {
+ while (sz) {
+ *dst = *src;
+ ++dst;
+ ++src;
+ --sz;
+ }
+}
+
diff --git a/link.ld b/link.ld
new file mode 100644
index 0000000..7e61bf3
--- /dev/null
+++ b/link.ld
@@ -0,0 +1,66 @@
+/* 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)
+ {
+ *(.multiboot2)
+ *(.text)
+ }
+
+ /* Read-only data. */
+ .rodata BLOCK(4K) : ALIGN(4K)
+ {
+ *(.rodata)
+ }
+
+ .init_array :
+ {
+ __init_array_start = .;
+ *(.init_array)
+ __init_array_end = .;
+ }
+
+ /* 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. */
+
+ .note :
+ {
+ *(.note)
+ *(.note*)
+ }
+}
+
diff --git a/memory.cpp b/memory.cpp
new file mode 100644
index 0000000..2cc15be
--- /dev/null
+++ b/memory.cpp
@@ -0,0 +1,87 @@
+#include "textoutput.hpp"
+
+#include <array>
+#include <cstdint>
+
+struct PageDirectory
+{
+ static constexpr std::uint32_t NotPresent = 0x2;
+
+ PageDirectory(): value(NotPresent) {}
+ PageDirectory(void *addr): value(reinterpret_cast<std::uint32_t>(addr) | 3) {}
+
+ std::uint32_t value;
+};
+static_assert(sizeof(PageDirectory) == sizeof(std::uint32_t));
+
+extern std::uint32_t lowerMem;
+extern std::uint32_t upperMem;
+extern TextOutput& term;
+
+static std::uintptr_t lowerFree = 0x400;
+static std::uintptr_t upperFree = 0x100000;
+
+alignas(4096)
+static std::array<PageDirectory, 1024> pageDirectory;
+
+alignas(4096)
+static std::array<std::uint32_t, 1024> pageTable;
+
+void memory_initialize()
+{
+ lowerMem -= 1024;
+
+ const auto totalKb = (lowerMem + upperMem) / 1024u;
+
+ term.write("Claiming ");
+ term.write(totalKb);
+ term.write(" kB for allocations...\n");
+
+ std::uint32_t addr = 0;
+ for (auto& p : pageTable) {
+ p = addr | 3; // supervisor, r/w, present
+ addr += 0x1000;
+ }
+
+ pageDirectory[0] = PageDirectory(pageTable.data());
+
+ asm volatile(R"(
+ mov %%eax, %%cr3
+ mov %%cr0, %%eax
+ or $0x80000000, %%eax
+ mov %%eax, %%cr0
+ )" :: "a"(pageDirectory.data()));
+
+ term.write("Paging enabled.\n");
+}
+
+static void *memory_alloc(std::size_t size)
+{
+ void *ret = nullptr;
+
+ if (lowerMem > size) {
+ ret = reinterpret_cast<void *>(lowerFree);
+ lowerFree += size;
+ lowerMem -= size;
+ } else if (upperMem > size) {
+ ret = reinterpret_cast<void *>(upperFree);
+ upperFree += size;
+ upperMem -= size;
+ } else {
+ // Uh oh!
+ term.write("!!! Kernel allocation failed !!!");
+ }
+
+ return ret;
+}
+
+void *operator new(std::size_t size)
+{
+ return memory_alloc(size);
+}
+
+void *operator new[](std::size_t size)
+{
+ return memory_alloc(size);
+}
+
diff --git a/memory.hpp b/memory.hpp
new file mode 100644
index 0000000..8955262
--- /dev/null
+++ b/memory.hpp
@@ -0,0 +1,7 @@
+#ifndef MEMORY_HPP
+#define MEMORY_HPP
+
+void memory_initialize();
+
+#endif // MEMORY_HPP
+
diff --git a/multiboot.cpp b/multiboot.cpp
new file mode 100644
index 0000000..0c34984
--- /dev/null
+++ b/multiboot.cpp
@@ -0,0 +1,41 @@
+#include "textoutput.hpp"
+
+#include <cstdint>
+
+extern TextOutput& term;
+
+std::uint32_t multiboot_magic;
+std::uint32_t *multiboot_ptr;
+
+std::uint32_t lowerMem = 0;
+std::uint32_t upperMem = 0;
+
+bool multiboot_initialize()
+{
+ if (multiboot_magic != 0x36d76289) {
+ term.write("Not multiboot!");
+ return false;
+ }
+
+ term.write("Found multiboot headers: ");
+
+ auto ptr = multiboot_ptr + 2;
+ while (ptr[0] != 0 && ptr[1] != 8) {
+ term.write(ptr[0]);
+ term.write(", ");
+
+ if (ptr[0] == 4) {
+ lowerMem = ptr[2] * 1024;
+ upperMem = ptr[3] * 1024;
+ }
+
+ auto next = reinterpret_cast<std::uintptr_t>(ptr);
+ next += ptr[1];
+ next = (next + 7) & ~7;
+ ptr = reinterpret_cast<std::uint32_t *>(next);
+ }
+
+ term.write('\n');
+ return true;
+}
+
diff --git a/multiboot.hpp b/multiboot.hpp
new file mode 100644
index 0000000..9916850
--- /dev/null
+++ b/multiboot.hpp
@@ -0,0 +1,7 @@
+#ifndef MULTIBOOT_HPP
+#define MULTIBOOT_HPP
+
+bool multiboot_initialize();
+
+#endif // MULTIBOOT_HPP
+
diff --git a/pic.cpp b/pic.cpp
new file mode 100644
index 0000000..437f384
--- /dev/null
+++ b/pic.cpp
@@ -0,0 +1,63 @@
+#include "pic.hpp"
+
+#include "portio.hpp"
+
+#define PIC1 0x20 /* IO base address for master PIC */
+#define PIC2 0xA0 /* IO base address for slave PIC */
+#define PIC1_COMMAND PIC1
+#define PIC1_DATA (PIC1+1)
+#define PIC2_COMMAND PIC2
+#define PIC2_DATA (PIC2+1)
+
+#define PIC_EOI 0x20 /* End-of-interrupt command code */
+
+#define ICW1_ICW4 0x01 /* Indicates that ICW4 will be present */
+#define ICW1_SINGLE 0x02 /* Single (cascade) mode */
+#define ICW1_INTERVAL4 0x04 /* Call address interval 4 (8) */
+#define ICW1_LEVEL 0x08 /* Level triggered (edge) mode */
+#define ICW1_INIT 0x10 /* Initialization - required! */
+
+#define ICW4_8086 0x01 /* 8086/88 (MCS-80/85) mode */
+#define ICW4_AUTO 0x02 /* Auto (normal) EOI */
+#define ICW4_BUF_SLAVE 0x08 /* Buffered mode/slave */
+#define ICW4_BUF_MASTER 0x0C /* Buffered mode/master */
+#define ICW4_SFNM 0x10 /* Special fully nested (not) */
+
+void pic_initialize()
+{
+ constexpr int offset1 = 0x20, offset2 = 0x28;
+ std::uint8_t a1, a2;
+
+ a1 = inb(PIC1_DATA); // save masks
+ a2 = inb(PIC2_DATA);
+
+ outb(PIC1_COMMAND, ICW1_INIT | ICW1_ICW4); // starts the initialization sequence (in cascade mode)
+ io_wait();
+ outb(PIC2_COMMAND, ICW1_INIT | ICW1_ICW4);
+ io_wait();
+ outb(PIC1_DATA, offset1); // ICW2: Master PIC vector offset
+ io_wait();
+ outb(PIC2_DATA, offset2); // ICW2: Slave PIC vector offset
+ io_wait();
+ outb(PIC1_DATA, 4); // ICW3: tell Master PIC that there is a slave PIC at IRQ2 (0000 0100)
+ io_wait();
+ outb(PIC2_DATA, 2); // ICW3: tell Slave PIC its cascade identity (0000 0010)
+ io_wait();
+
+ outb(PIC1_DATA, ICW4_8086); // ICW4: have the PICs use 8086 mode (and not 8080 mode)
+ io_wait();
+ outb(PIC2_DATA, ICW4_8086);
+ io_wait();
+
+ outb(PIC1_DATA, a1); // restore saved masks.
+ outb(PIC2_DATA, a2);
+}
+
+void pic_eoi(std::uint8_t irq)
+{
+ if (irq >= 8)
+ outb(PIC2_COMMAND, PIC_EOI);
+
+ outb(PIC1_COMMAND, PIC_EOI);
+}
+
diff --git a/pic.hpp b/pic.hpp
new file mode 100644
index 0000000..17abd55
--- /dev/null
+++ b/pic.hpp
@@ -0,0 +1,12 @@
+#ifndef PIC_HPP
+#define PIC_HPP
+
+#include <cstdint>
+
+/* reinitialize the PIC controllers, giving them specified vector offsets
+ rather than 8h and 70h, as configured by default */
+void pic_initialize();
+void pic_eoi(std::uint8_t irq);
+
+#endif // PIC_HPP
+
diff --git a/pit.cpp b/pit.cpp
new file mode 100644
index 0000000..4cafc57
--- /dev/null
+++ b/pit.cpp
@@ -0,0 +1,39 @@
+#include "pit.hpp"
+#include "idt.hpp"
+#include "portio.hpp"
+#include "tasking.hpp"
+
+static volatile std::uint32_t ticks = 0;
+
+static void timer_callback(const Registers& regs)
+{
+ ticks = ticks + 1;
+
+ schedule(const_cast<Registers&>(regs));
+}
+
+void pit_initialize(std::uint32_t frequency)
+{
+ // Firstly, register our timer callback.
+ idt_register_callback(32, timer_callback);
+
+ // The value we send to the PIT is the value to divide it's input clock
+ // (1193180 Hz) by, to get our required frequency. Important to note is
+ // that the divisor must be small enough to fit into 16-bits.
+ auto divisor = 1193180 / frequency;
+
+ // Send the command byte.
+ outb(0x43, 0x36);
+
+ // Send the frequency divisor.
+ outb(0x40, divisor & 0xFF);
+ outb(0x40, (divisor >> 8) & 0xFF);
+}
+
+void pit_busy_wait(std::int32_t tks)
+{
+ const auto end = ticks + tks;
+ while (end - ticks > 0)
+ asm volatile("nop");
+}
+
diff --git a/pit.hpp b/pit.hpp
new file mode 100644
index 0000000..5c2796c
--- /dev/null
+++ b/pit.hpp
@@ -0,0 +1,10 @@
+#ifndef PIT_HPP
+#define PIT_HPP
+
+#include <cstdint>
+
+void pit_initialize(std::uint32_t frequency);
+void pit_busy_wait(std::int32_t tks);
+
+#endif // PIT_HPP
+
diff --git a/portio.hpp b/portio.hpp
new file mode 100644
index 0000000..0a10651
--- /dev/null
+++ b/portio.hpp
@@ -0,0 +1,24 @@
+#ifndef PORTIO_HPP
+#define PORTIO_HPP
+
+#include <cstdint>
+
+inline void outb(std::uint16_t port, std::uint8_t val)
+{
+ asm volatile("outb %b0, %w1" :: "a"(val), "Nd"(port) : "memory");
+}
+
+inline std::uint8_t inb(std::uint16_t port)
+{
+ std::uint8_t val;
+ asm volatile("inb %w1, %b0" : "=a"(val) : "Nd"(port) : "memory");
+ return val;
+}
+
+inline void io_wait()
+{
+ outb(0x80, 0);
+}
+
+#endif // PORTIO_HPP
+
diff --git a/tasking.cpp b/tasking.cpp
new file mode 100644
index 0000000..9e2cd49
--- /dev/null
+++ b/tasking.cpp
@@ -0,0 +1,60 @@
+#include "tasking.hpp"
+
+#include <array>
+
+struct Task
+{
+ Registers regs;
+ bool valid = false;
+};
+
+static std::array<Task, 4> tasks;
+static int current = -1;
+
+void schedule(Registers& regs)
+{
+ if (current < 0)
+ return;
+
+ tasks[current].regs = regs;
+
+ do {
+ if (++current >= tasks.size())
+ current = 0;
+ } while (!tasks[current].valid);
+
+ regs = tasks[current].regs;
+}
+
+void tasking_initialize()
+{
+ tasks[0].valid = true;
+ current = 0;
+ asm volatile("int $0x20");
+}
+
+bool tasking_spawn(void (*entry)(), unsigned ssize)
+{
+ int i = -1;
+ for (i = 0; i < tasks.size(); ++i) {
+ if (!tasks[i].valid)
+ break;
+ }
+
+ if (i < 0)
+ return false;
+
+ tasks[i] = Task();
+
+ auto& r = tasks[i].regs;
+ auto stack = reinterpret_cast<std::uint32_t>(new std::uint8_t[ssize]);
+ r.ebp = stack + ssize;
+ r.esp = r.ebp;
+ r.eip = reinterpret_cast<std::uint32_t>(entry);
+ r.cs = 0x8;
+ r.eflags = tasks[current].regs.eflags;
+
+ tasks[i].valid = true;
+ return true;
+}
+
diff --git a/tasking.hpp b/tasking.hpp
new file mode 100644
index 0000000..5e03f25
--- /dev/null
+++ b/tasking.hpp
@@ -0,0 +1,12 @@
+#ifndef TASKING_HPP
+#define TASKING_HPP
+
+#include "idt.hpp"
+
+void tasking_initialize();
+bool tasking_spawn(void (*entry)(), unsigned ssize);
+
+void schedule(Registers& regs);
+
+#endif // TASKING_HPP
+
diff --git a/textoutput.hpp b/textoutput.hpp
new file mode 100644
index 0000000..7ef12fa
--- /dev/null
+++ b/textoutput.hpp
@@ -0,0 +1,44 @@
+#ifndef TEXTOUTPUT_HPP
+#define TEXTOUTPUT_HPP
+
+class TextOutput
+{
+public:
+ virtual void write(char c) noexcept = 0;
+
+ void write(const char *s) noexcept {
+ if (s) {
+ while (*s)
+ write(*s++);
+ }
+ }
+
+ void write(int n) noexcept {
+ char buf[32];
+ auto ptr = buf + sizeof(buf);
+
+ *--ptr = '\0';
+ do {
+ *--ptr = "0123456789"[n % 10];
+ n /= 10;
+ } while (n);
+
+ write(ptr);
+ }
+
+ void write(unsigned n) noexcept {
+ char buf[32];
+ auto ptr = buf + sizeof(buf);
+
+ *--ptr = '\0';
+ do {
+ *--ptr = "0123456789"[n % 10];
+ n /= 10;
+ } while (n);
+
+ write(ptr);
+ }
+};
+
+#endif // TEXTOUTPUT_HPP
+
diff --git a/vgaterminal.cpp b/vgaterminal.cpp
new file mode 100644
index 0000000..8158b14
--- /dev/null
+++ b/vgaterminal.cpp
@@ -0,0 +1,54 @@
+#include "portio.hpp"
+#include "vgaterminal.hpp"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <utility>
+
+void VGATerminal::write(char c) noexcept
+{
+ switch (c) {
+ case '\n':
+ offset += Width;
+ [[fallthrough]];
+ case '\r':
+ offset -= offset % Width;
+ break;
+ default:
+ checkpos();
+ put(c);
+ updatecursor();
+ break;
+ }
+}
+
+void VGATerminal::put(char c) noexcept
+{
+ std::uint16_t cell = c
+ | (std::to_underlying(foreground) << 8)
+ | (std::to_underlying(background) << 12);
+
+ auto ptr = reinterpret_cast<std::uint16_t *>(Videoram);
+ ptr[offset++] = cell;
+}
+
+void VGATerminal::checkpos() noexcept
+{
+ if (offset >= Width * Height) {
+ auto ptr = reinterpret_cast<std::uint16_t *>(Videoram);
+ const auto end = ptr + Width * Height;
+ std::copy(ptr + Width, end, ptr);
+ std::fill(end - Width, end, 0);
+ offset = Width * Height - Width;
+ }
+}
+
+void VGATerminal::updatecursor() const noexcept
+{
+ outb(0x03d4, 0x0f);
+ outb(0x03d5, static_cast<std::uint8_t>(offset));
+ outb(0x03d4, 0x0e);
+ outb(0x03d5, static_cast<std::uint8_t>(offset >> 8));
+}
+
diff --git a/vgaterminal.hpp b/vgaterminal.hpp
new file mode 100644
index 0000000..9f8d5f3
--- /dev/null
+++ b/vgaterminal.hpp
@@ -0,0 +1,51 @@
+#ifndef VGATERMINAL_HPP
+#define VGATERMINAL_HPP
+
+#include "textoutput.hpp"
+
+#include <cstddef>
+#include <cstdint>
+
+class VGATerminal : public TextOutput
+{
+public:
+ enum class Color : std::uint8_t
+ {
+ Black = 0,
+ Blue,
+ Green,
+ Cyan,
+ Red,
+ Magenta,
+ Brown,
+ LightGray,
+ DarkGray,
+ LightBlue,
+ LightGreen,
+ LightCyan,
+ LightRed,
+ LightMagenta,
+ LightBrown,
+ White
+ };
+
+ using enum Color;
+
+ virtual void write(char c) noexcept final;
+
+private:
+ static constexpr std::uintptr_t Videoram = 0xB8000;
+ static constexpr unsigned Width = 80;
+ static constexpr unsigned Height = 25;
+
+ unsigned offset = 0;
+ Color foreground = LightGray;
+ Color background = Black;
+
+ void put(char c) noexcept;
+ void checkpos() noexcept;
+ void updatecursor() const noexcept;
+};
+
+#endif // VGATERMINAL_HPP
+