10 Commits

Author SHA1 Message Date
42fc169e10 Interrupt Dispatch and Handling (for first common vectors) 2025-12-22 21:04:45 +01:00
d0b4da0596 IDT: set entry, load into IDTR, interrupt stub + dispatcher for common faults 2025-12-22 19:38:50 +01:00
0031c2fe03 Woops.. it wasnt nonsense after all 2025-12-22 11:27:39 +01:00
282a423387 Delete GCH nonsense 2025-12-22 11:26:59 +01:00
c43be0bddd Merge pull request #3 from xamidev/gdt
GDT init (load + flush)
2025-12-22 11:24:15 +01:00
6fc7266716 GDT init (load + flush) 2025-12-22 11:20:24 +01:00
29deb20cd7 Merge pull request #2 from xamidev/serial
Serial communication
2025-12-21 20:35:02 +01:00
62302e03d5 Add: init serial + getting text out of it 2025-12-21 20:33:48 +01:00
e6f4200ae9 rename stuff + add GDB debug rule 2025-12-21 15:59:14 +01:00
c8df8934b5 Merge pull request #1 from xamidev/hello-world
Hello world
2025-12-21 15:41:10 +01:00
16 changed files with 732 additions and 78 deletions

4
.gitignore vendored
View File

@@ -1,5 +1,9 @@
limine
kernel
pepperk
iso_root
*.o
*.iso
*.gch
*/*.gch
*/*/*.gch

View File

@@ -1,8 +1,9 @@
build:
rm -f *.o
x86_64-elf-gcc -c -I src src/io/term.c src/io/printf.c src/kmain.c -Wall -Wextra -std=gnu99 -nostdlib -ffreestanding -fno-stack-protector -fno-stack-check -fno-PIC -ffunction-sections -fdata-sections -mcmodel=kernel
x86_64-elf-gcc -g -c -I src src/idt/idt.c src/mem/utils.c src/mem/gdt.c src/io/serial.c src/io/term.c src/io/printf.c src/kmain.c -Wall -Wextra -std=gnu99 -nostdlib -ffreestanding -fno-stack-protector -fno-stack-check -fno-PIC -ffunction-sections -fdata-sections -mcmodel=kernel
objcopy -O elf64-x86-64 -B i386 -I binary zap-light16.psf zap-light16.o
x86_64-elf-ld -o kernel -T linker.ld *.o
nasm -f elf64 src/idt/idt.S -o idt_stub.o
x86_64-elf-ld -o pepperk -T linker.ld *.o
limine/limine:
rm -rf limine
@@ -12,7 +13,7 @@ limine/limine:
build-iso: limine/limine build
rm -rf iso_root
mkdir -p iso_root/boot
cp -v kernel iso_root/boot
cp -v pepperk iso_root/boot
mkdir -p iso_root/boot/limine
cp -v limine.conf iso_root/boot/limine
mkdir -p iso_root/EFI/BOOT
@@ -23,11 +24,15 @@ build-iso: limine/limine build
-no-emul-boot -boot-load-size 4 -boot-info-table -hfsplus \
-apm-block-size 2048 --efi-boot boot/limine/limine-uefi-cd.bin \
-efi-boot-part --efi-boot-image --protective-msdos-label \
iso_root -o kernel.iso
./limine/limine bios-install kernel.iso
iso_root -o pepper.iso
./limine/limine bios-install pepper.iso
debug:
qemu-system-x86_64 -drive file=pepper.iso -s -S -d int -no-reboot &
gdb pepperk --command=debug.gdb
run: build-iso
qemu-system-x86_64 -cdrom kernel.iso
qemu-system-x86_64 -cdrom pepper.iso -serial stdio
clean:
rm -rf *.o kernel iso_root kernel.iso limine
rm -rf *.o pepperk iso_root pepper.iso limine

View File

@@ -13,4 +13,8 @@ PepperOS wouldn't be possible without the following freely-licensed software:
- the [Limine](https://codeberg.org/Limine/Limine) portable bootloader
- Marco Paland's freestanding [printf implementation](https://github.com/mpaland)
- the [ZAP](https://www.zap.org.au/projects/console-fonts-zap/) PSF console fonts
- the [ZAP](https://www.zap.org.au/projects/console-fonts-zap/) PSF console fonts
...and without these amazing resources:
- the [OSDev](https://osdev.org) wiki & forums

2
debug.gdb Normal file
View File

@@ -0,0 +1,2 @@
target remote localhost:1234
set disassembly-flavor intel

View File

@@ -3,4 +3,4 @@ timeout: 3
/PepperOS
protocol: limine
path: boot():/boot/kernel
path: boot():/boot/pepperk

236
src/idt/idt.S Normal file
View File

@@ -0,0 +1,236 @@
; Assembly stub for the IDT
bits 64
extern interrupt_dispatch
global interrupt_stub
global vector_0_handler
global vector_1_handler
global vector_2_handler
global vector_3_handler
global vector_4_handler
global vector_5_handler
global vector_6_handler
global vector_7_handler
global vector_8_handler
global vector_9_handler
global vector_10_handler
global vector_11_handler
global vector_12_handler
global vector_13_handler
global vector_14_handler
global vector_15_handler
global vector_16_handler
global vector_17_handler
global vector_18_handler
global vector_19_handler
global vector_20_handler
global vector_21_handler
interrupt_stub:
; We'll push all general-purpose registers to the stack,
; so they're intact and don't bother the code that was
; executed when the interrupt happened.
; (except rsp because it will already be saved in the iret frame)
push rax
push rbx
push rcx
push rdx
push rsi
push rdi
push rsp
push rbp
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15
; Put stack pointer as first argument of our function
mov rdi, rsp
call interrupt_dispatch
; What the function returns (new stack pointer) is saved in rbp
mov rsp, rax
pop r15
pop r14
pop r13
pop r12
pop r11
pop r10
pop r9
pop r8
pop rbp
pop rsp
pop rdi
pop rsi
pop rdx
pop rcx
pop rbx
pop rax
; Removing the error code and vector number so stack doesn't
; get corrupted
add rsp, 16
; Restore ss, rsp, rflags, cs, rip of code that was executing
; before the interrupt
iret
; Vector handlers will be 16-byte aligned so that we can loop over them
; like <vector_no> * 16 to get each one's address
; Divide Error
align 16
vector_0_handler:
; error code (nothing, so we push a dummy 0 quadword, 64bits/8bytes long)
push qword 0
; vector number (so our interrupt stub knows which one it is)
push qword 0
jmp interrupt_stub
; Debug Exception
align 16
vector_1_handler:
push qword 0
push qword 1
jmp interrupt_stub
; NMI
align 16
vector_2_handler:
push qword 0
push qword 2
jmp interrupt_stub
; Breakpoint
align 16
vector_3_handler:
push qword 0
push qword 3
jmp interrupt_stub
; Overflow
align 16
vector_4_handler:
push qword 0
push qword 4
jmp interrupt_stub
; BOUND Range exceeded
align 16
vector_5_handler:
push qword 0
push qword 5
jmp interrupt_stub
; Invalid Opcode
align 16
vector_6_handler:
push qword 0
push qword 6
jmp interrupt_stub
; Device Not Available
align 16
vector_7_handler:
push qword 0
push qword 7
jmp interrupt_stub
; Double Fault
align 16
vector_8_handler:
; No error code, we only push vector number
push qword 1
jmp interrupt_stub
; Coprocessor Segment Overrun
align 16
vector_9_handler:
push qword 9
jmp interrupt_stub
; Invalid TSS
align 16
vector_10_handler:
push qword 10
jmp interrupt_stub
; Segment Not Present
align 16
vector_11_handler:
push qword 11
jmp interrupt_stub
; Stack-Segment Fault
align 16
vector_12_handler:
push qword 12
jmp interrupt_stub
; General Protection
align 16
vector_13_handler:
push qword 13
jmp interrupt_stub
; Page Fault
align 16
vector_14_handler:
push qword 14
jmp interrupt_stub
; Intel reserved
align 16
vector_15_handler:
push qword 0
push qword 15
jmp interrupt_stub
; x87 FPU Floating-Point Error
align 16
vector_16_handler:
push qword 0
push qword 16
jmp interrupt_stub
; Alignment Check
align 16
vector_17_handler:
push qword 17
jmp interrupt_stub
; Machine Check
align 16
vector_18_handler:
push qword 0
push qword 18
jmp interrupt_stub
; SIMD Floating-Point Exception
align 16
vector_19_handler:
push qword 0
push qword 19
jmp interrupt_stub
; Virtualization Exception
align 16
vector_20_handler:
push qword 0
push qword 20
jmp interrupt_stub
; Control Protection Exception
align 16
vector_21_handler:
push qword 21
jmp interrupt_stub
; The others are reserved (22->31) or external (32->255) interrupts

128
src/idt/idt.c Normal file
View File

@@ -0,0 +1,128 @@
#include "idt.h"
#include <stdint.h>
#include <stddef.h>
#include "../io/serial.h"
struct interrupt_descriptor idt[256];
struct idtr idt_reg;
// Address to our first interrupt handler
extern char vector_0_handler[];
void idt_set_entry(uint8_t vector, void* handler, uint8_t dpl)
{
uint64_t handler_addr = (uint64_t)handler;
struct interrupt_descriptor* entry = &idt[vector];
// Address is split in three parts so we right-shift progressively to get it all
entry->address_low = handler_addr & 0xFFFF;
entry->address_mid = (handler_addr >> 16) & 0xFFFF;
entry->address_high = handler_addr >> 32;
// Kernel code selector (as set in GDT)
entry->selector = 0x8;
// Interrupt gate, present, DPL (having: max DPL = 3)
entry->flags = 0b1110 | ((dpl & 0b11) << 5) | (1 << 7);
// We won't use IST for now
entry->ist = 0;
}
void idt_load(void* idt_addr)
{
// "limit" = "size" = Size of the IDT - 1 byte = (16*256)-1 = 0xFFF
idt_reg.limit = 0xFFF;
idt_reg.base = (uint64_t)idt_addr;
asm volatile("lidt %0" :: "m"(idt_reg));
}
void idt_init()
{
// We set 256 entries, but we have only the first few stubs.
// Undefined behavior?
for (size_t i=0; i<256; i++)
{
// Each vector handler is 16-byte aligned, so <vector_no>*16 = address of that handler
idt_set_entry(i, vector_0_handler + (i*16), 0);
}
idt_load(&idt);
serial_kputs("kernel: idt: Initialized IDT!\n");
}
struct cpu_status_t* interrupt_dispatch(struct cpu_status_t* context)
{
switch(context->vector_number)
{
case 0:
serial_kputs("kernel: idt: Divide Error!\n");
break;
case 1:
serial_kputs("kernel: idt: Debug Exception!\n");
break;
case 2:
serial_kputs("kernel: idt: NMI Interrupt!\n");
break;
case 3:
serial_kputs("kernel: idt: Breakpoint Interrupt!\n");
break;
case 4:
serial_kputs("kernel: idt: Overflow Trap!\n");
break;
case 5:
serial_kputs("kernel: idt: BOUND Range Exceeded!\n");
break;
case 6:
serial_kputs("kernel: idt: Invalid Opcode!\n");
break;
case 7:
serial_kputs("kernel: idt: Device Not Available!\n");
break;
case 8:
serial_kputs("kernel: idt: Double Fault!\n");
break;
case 9:
serial_kputs("kernel: idt: Coprocessor Segment Overrun!\n");
break;
case 10:
serial_kputs("kernel: idt: Invalid TSS!\n");
break;
case 11:
serial_kputs("kernel: idt: Segment Not Present!\n");
break;
case 12:
serial_kputs("kernel: idt: Stack-Segment Fault!\n");
break;
case 13:
serial_kputs("kernel: idt: General Protection Fault!\n");
break;
case 14:
serial_kputs("kernel: idt: Page Fault!\n");
break;
case 15:
serial_kputs("kernel: idt: Intel Reserved Interrupt! (Achievement unlocked: How Did We Get Here?)\n");
break;
case 16:
serial_kputs("kernel: idt: x87 Floating-Point Error!\n");
break;
case 17:
serial_kputs("kernel: idt: Alignment Check Fault!\n");
break;
case 18:
serial_kputs("kernel: idt: Machine Check!\n");
break;
case 19:
serial_kputs("kernel: idt: SIMD Floating-Point Exception!\n");
break;
case 20:
serial_kputs("kernel: idt: Virtualization Exception!\n");
break;
case 21:
serial_kputs("kernel: idt: Control Protection Exception!\n");
break;
default:
serial_kputs("kernel: idt: Unexpected interrupt\n");
break;
}
return context;
}

57
src/idt/idt.h Normal file
View File

@@ -0,0 +1,57 @@
#ifndef IDT_H
#define IDT_H
#include <stdint.h>
void idt_init();
struct interrupt_descriptor
{
uint16_t address_low;
uint16_t selector;
uint8_t ist;
uint8_t flags;
uint16_t address_mid;
uint32_t address_high;
uint32_t reserved;
} __attribute__((packed));
struct idtr
{
uint16_t limit;
uint64_t base;
} __attribute__((packed));
// All general-purpose registers (except rsp) as stored on the stack,
// plus the values we pushed (vector number, error code) and the iret frame
// In reverse order because the stack grows downwards.
struct cpu_status_t
{
uint64_t r15;
uint64_t r14;
uint64_t r13;
uint64_t r12;
uint64_t r11;
uint64_t r10;
uint64_t r9;
uint64_t r8;
uint64_t rbp;
uint64_t rsp;
uint64_t rdi;
uint64_t rsi;
uint64_t rdx;
uint64_t rcx;
uint64_t rbx;
uint64_t rax;
uint64_t vector_number;
uint64_t error_code;
uint64_t iret_rip;
uint64_t iret_cs;
uint64_t iret_flags;
uint64_t iret_rsp;
uint64_t iret_ss;
};
#endif

62
src/io/serial.c Normal file
View File

@@ -0,0 +1,62 @@
#include "../kernel.h"
#include "serial.h"
void outb(int port, unsigned char data)
{
__asm__ __volatile__("outb %%al, %%dx" :: "a" (data),"d" (port));
}
unsigned char inb(int port)
{
unsigned char data = 0;
__asm__ __volatile__("inb %%dx, %%al" : "=a" (data) : "d" (port));
return data;
}
// COM1
#define PORT 0x3F8
int serial_init()
{
outb(PORT + 1, 0x00); // Disable all interrupts
outb(PORT + 3, 0x80); // Enable DLAB (set baud rate divisor)
outb(PORT + 0, 0x03); // Set divisor to 3 (lo byte) 38400 baud
outb(PORT + 1, 0x00); // (hi byte)
outb(PORT + 3, 0x03); // 8 bits, no parity, one stop bit
outb(PORT + 2, 0xC7); // Enable FIFO, clear them, with 14-byte threshold
outb(PORT + 4, 0x0B); // IRQs enabled, RTS/DSR set
outb(PORT + 4, 0x1E); // Set in loopback mode, test the serial chip
outb(PORT + 0, 0xAE); // Test serial chip (send byte 0xAE and check if serial returns same byte)
if (inb(PORT) != 0xAE)
{
return -EIO;
}
// Set normal operation mode
outb(PORT + 4, 0x0F);
serial_kputs("\n\nkernel: serial: Serial initialization OK!\n");
return 0;
}
static int is_transmit_empty()
{
return inb(PORT + 5) & 0x20;
}
void write_serial(char c)
{
while (!is_transmit_empty()); // wait for free spot
outb(PORT, c);
}
void serial_kputs(const char* str)
{
unsigned int i=0;
while (str[i])
{
write_serial(str[i]);
i++;
}
}

10
src/io/serial.h Normal file
View File

@@ -0,0 +1,10 @@
#ifndef SERIAL_H
#define SERIAL_H
void outb(int port, unsigned char data);
unsigned char inb(int port);
int serial_init();
void serial_kputs(const char* str);
#endif

View File

@@ -3,7 +3,8 @@
enum ErrorCodes
{
ENOMEM
ENOMEM,
EIO
};
#endif

View File

@@ -3,6 +3,10 @@
#include <limine.h>
#include "io/term.h"
#include "io/printf.h"
#include "io/serial.h"
#include "mem/gdt.h"
#include "mem/utils.h"
#include "idt/idt.h"
// Limine version used
__attribute__((used, section(".limine_requests")))
@@ -23,74 +27,6 @@ static volatile LIMINE_REQUESTS_END_MARKER;
struct limine_framebuffer* framebuffer;
// We won't be linked to standard library, but still need the basic mem* functions
// so everything goes allright with the compiler
// We use the "restrict" keyword on pointers so that the compiler knows it can
// do more optimization on them (and as it's a much used function, it's good to
// be able to do that)
void* memcpy(void* restrict dest, const void* restrict src, size_t n)
{
uint8_t* restrict pdest = (uint8_t* restrict)dest;
const uint8_t* restrict psrc = (const uint8_t* restrict)src;
for (size_t i=0; i<n; i++)
{
pdest[i] = psrc[i];
}
return dest;
}
void* memset(void* s, int c, size_t n)
{
uint8_t* p = (uint8_t*)s;
for (size_t i=0; i<n; i++)
{
p[i] = (uint8_t)c;
}
return s;
}
void* memmove(void *dest, const void* src, size_t n)
{
uint8_t* pdest = (uint8_t*)dest;
const uint8_t* psrc = (uint8_t*)src;
if (src > dest)
{
for (size_t i=0; i<n; i++)
{
pdest[i] = psrc[i];
}
} else if (src < dest)
{
for (size_t i=n; i>0; i--)
{
pdest[i-1] = psrc[i-1];
}
}
return dest;
}
int memcmp(const void* s1, const void* s2, size_t n)
{
const uint8_t* p1 = (const uint8_t*)s1;
const uint8_t* p2 = (const uint8_t*)s2;
for (size_t i=0; i<n; i++)
{
if (p1[i] != p2[i])
{
return p1[i] < p2[i] ? -1 : 1;
}
}
return 0;
}
// Panic
static void hcf()
{
@@ -100,6 +36,20 @@ static void hcf()
}
}
static inline void trigger_div0(void)
{
asm volatile (
"mov $1, %%rax\n"
"xor %%rdx, %%rdx\n"
"xor %%rcx, %%rcx\n" // divisor = 0
"idiv %%rcx\n"
:
:
: "rax", "rcx", "rdx"
);
}
// This is our entry point
void kmain()
{
@@ -111,8 +61,14 @@ void kmain()
if (term_init()) hcf();
if (serial_init()) kputs("kernel: serial: error: Cannot init serial communication!");
gdt_init();
idt_init();
// Draw something
printf("%s, %s!", "Hello", "world");
trigger_div0();
hcf();
}

82
src/mem/gdt.c Normal file
View File

@@ -0,0 +1,82 @@
#include "gdt.h"
#include <stdint.h>
#include "../io/serial.h"
// Descriptors are 8-byte wide (64bits)
// So the selectors will be (in bytes): 0x0, 0x8, 0x10, 0x18, etc..
uint64_t gdt_entries[NUM_GDT_ENTRIES];
struct GDTR gdtr;
static void gdt_load()
{
asm("lgdt %0" : : "m"(gdtr));
}
static void gdt_flush()
{
// Here, 0x8 is the kernel code selector
// and 0x10 is the kernel data selector
asm volatile (
"mov $0x10, %%ax \n" // Reload segments with kernel data selector
"mov %%ax, %%ds \n"
"mov %%ax, %%es \n"
"mov %%ax, %%fs \n"
"mov %%ax, %%gs \n"
"mov %%ax, %%ss \n"
"pushq $0x8 \n" // CS reload
"lea 1f(%%rip), %%rax \n"
"push %%rax \n"
"lretq \n"
"1: \n" // Execution continues here after CS reload
:
:
: "rax", "memory"
);
}
void gdt_init()
{
// Null descriptor (required)
gdt_entries[0] = 0;
// Kernel code segment
uint64_t kernel_code = 0;
kernel_code |= 0b1101 << 8; // Selector type: accessed, read-enable, no conforming
kernel_code |= 1 << 12; // not a system descriptor
kernel_code |= 0 << 13; // DPL field = 0
kernel_code |= 1 << 15; // Present
kernel_code |= 1 << 21; // Long mode
// Left shift 32 bits so we place our stuff in the upper 32 bits of the descriptor.
// The lower 32 bits contain limit and part of base and therefore are ignored in Long Mode
// (because we'll use paging; segmentation is used only for legacy)
gdt_entries[1] = kernel_code << 32;
uint64_t kernel_data = 0;
kernel_data |= 0b0011 << 8;
kernel_data |= 1 << 12;
kernel_data |= 0 << 13;
kernel_data |= 1 << 15;
kernel_data |= 1 << 21;
gdt_entries[2] = kernel_data << 32;
// We re-use the kernel descriptors here, and just update their DPL fields
// (Descriptor privilege level) from ring 0 -> to ring 3 (userspace)
uint64_t user_code = kernel_code | (3 << 13);
gdt_entries[3] = user_code;
uint64_t user_data = kernel_data | (3 << 13);
gdt_entries[4] = user_data;
// The -1 subtraction is some wizardry explained in the OSDev wiki -> GDT
gdtr.limit = NUM_GDT_ENTRIES * sizeof(uint64_t) - 1;
gdtr.address = (uint64_t)gdt_entries;
// Load the GDT we created, flush the old one
gdt_load();
gdt_flush();
serial_kputs("kernel: gdt: Initialized GDT!\n");
}

26
src/mem/gdt.h Normal file
View File

@@ -0,0 +1,26 @@
#ifndef GDT_H
#define GDT_H
#include <stdint.h>
// We're using the GDT for segmentation, but as we want to target Long Mode,
// we'll only use this as a requirement for paging, not more.
// This means base 0 and no limit (whole address space)
#define NUM_GDT_ENTRIES 5
#define NULL_SELECTOR 0x00
#define KERNEL_CODE_SEGMENT 0x08
#define KERNEL_DATA_SEGMENT 0x10
#define USER_CODE_SEGMENT 0x18
#define USER_DATA_SEGMENT 0x20
struct GDTR
{
uint16_t limit;
uint64_t address;
} __attribute__((packed));
void gdt_init();
#endif

70
src/mem/utils.c Normal file
View File

@@ -0,0 +1,70 @@
#include <stddef.h>
#include <stdint.h>
// We won't be linked to standard library, but still need the basic mem* functions
// so everything goes allright with the compiler
// We use the "restrict" keyword on pointers so that the compiler knows it can
// do more optimization on them (and as it's a much used function, it's good to
// be able to do that)
void* memcpy(void* restrict dest, const void* restrict src, size_t n)
{
uint8_t* restrict pdest = (uint8_t* restrict)dest;
const uint8_t* restrict psrc = (const uint8_t* restrict)src;
for (size_t i=0; i<n; i++)
{
pdest[i] = psrc[i];
}
return dest;
}
void* memset(void* s, int c, size_t n)
{
uint8_t* p = (uint8_t*)s;
for (size_t i=0; i<n; i++)
{
p[i] = (uint8_t)c;
}
return s;
}
void* memmove(void *dest, const void* src, size_t n)
{
uint8_t* pdest = (uint8_t*)dest;
const uint8_t* psrc = (uint8_t*)src;
if (src > dest)
{
for (size_t i=0; i<n; i++)
{
pdest[i] = psrc[i];
}
} else if (src < dest)
{
for (size_t i=n; i>0; i--)
{
pdest[i-1] = psrc[i-1];
}
}
return dest;
}
int memcmp(const void* s1, const void* s2, size_t n)
{
const uint8_t* p1 = (const uint8_t*)s1;
const uint8_t* p2 = (const uint8_t*)s2;
for (size_t i=0; i<n; i++)
{
if (p1[i] != p2[i])
{
return p1[i] < p2[i] ? -1 : 1;
}
}
return 0;
}

11
src/mem/utils.h Normal file
View File

@@ -0,0 +1,11 @@
#ifndef MEM_UTILS_H
#define MEM_UTILS_H
#include <stddef.h>
void* memcpy(void* restrict dest, const void* restrict src, size_t n);
void* memset(void* s, int c, size_t n);
void* memmove(void *dest, const void* src, size_t n);
int memcmp(const void* s1, const void* s2, size_t n);
#endif