]> code.bitgloo.com Git - clyne/osdev.git/commitdiff
wip: initial commit
authorClyne Sullivan <clyne@bitgloo.com>
Fri, 27 Sep 2024 10:02:49 +0000 (06:02 -0400)
committerClyne Sullivan <clyne@bitgloo.com>
Fri, 27 Sep 2024 10:02:49 +0000 (06:02 -0400)
24 files changed:
.gitignore [new file with mode: 0644]
Makefile [new file with mode: 0644]
boot.s [new file with mode: 0644]
gdt.cpp [new file with mode: 0644]
gdt.hpp [new file with mode: 0644]
idt.cpp [new file with mode: 0644]
idt.hpp [new file with mode: 0644]
iso/boot/grub/grub.cfg [new file with mode: 0644]
kernel.cpp [new file with mode: 0644]
link.ld [new file with mode: 0644]
memory.cpp [new file with mode: 0644]
memory.hpp [new file with mode: 0644]
multiboot.cpp [new file with mode: 0644]
multiboot.hpp [new file with mode: 0644]
pic.cpp [new file with mode: 0644]
pic.hpp [new file with mode: 0644]
pit.cpp [new file with mode: 0644]
pit.hpp [new file with mode: 0644]
portio.hpp [new file with mode: 0644]
tasking.cpp [new file with mode: 0644]
tasking.hpp [new file with mode: 0644]
textoutput.hpp [new file with mode: 0644]
vgaterminal.cpp [new file with mode: 0644]
vgaterminal.hpp [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..540202a
--- /dev/null
@@ -0,0 +1,5 @@
+*.bin
+*.iso
+*.o
+*.sw*
+iso/boot/*.bin
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
index 0000000..24aafbd
--- /dev/null
@@ -0,0 +1,4 @@
+menuentry "myos" {
+       multiboot2 /boot/myos.bin
+}
+
diff --git a/kernel.cpp b/kernel.cpp
new file mode 100644 (file)
index 0000000..41c3a61
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..2cc15be
--- /dev/null
@@ -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 (file)
index 0000000..8955262
--- /dev/null
@@ -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 (file)
index 0000000..0c34984
--- /dev/null
@@ -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 (file)
index 0000000..9916850
--- /dev/null
@@ -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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
index 0000000..0a10651
--- /dev/null
@@ -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 (file)
index 0000000..9e2cd49
--- /dev/null
@@ -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 (file)
index 0000000..5e03f25
--- /dev/null
@@ -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 (file)
index 0000000..7ef12fa
--- /dev/null
@@ -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 (file)
index 0000000..8158b14
--- /dev/null
@@ -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 (file)
index 0000000..9f8d5f3
--- /dev/null
@@ -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
+