Compare commits

2 Commits

Author SHA1 Message Date
7c45406c23 Add PIC handling (human) 2026-02-23 11:05:41 +01:00
AI
e4cc638a8d Implement GDT and basic IDT setup
This commit adds GDT initialization with proper code/data segments and reloads CS.

It also adds the initial IDT structure and loads an empty IDT.

Build configuration updated to disable SSE/MMX to prevent compiler generation of unsupported instructions in early boot.
2026-02-23 10:05:17 +00:00
12 changed files with 229 additions and 9 deletions

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@ release/
debug_grub/
*_output.txt
snippet.*
qemu.log
iso_output.txt

View File

@@ -4,7 +4,7 @@ project(ClaudeOS C ASM)
set(CMAKE_C_STANDARD 99)
# We are building a kernel, so we don't want standard libraries
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffreestanding -m32 -fno-pie -fno-pic -fno-builtin -fno-stack-protector -g -O2 -Wall -Wextra")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffreestanding -m32 -fno-pie -fno-pic -fno-builtin -fno-stack-protector -mno-sse -mno-mmx -g -O2 -Wall -Wextra")
set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -m32 -fno-pie -fno-pic")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -m32 -nostdlib -no-pie")

View File

@@ -41,7 +41,9 @@ Once a task is completed, it should be checked off.
- [x] Create initial build system
- [x] Setup a simple kernel that writes `Hello, world` to Qemu debug port
- [x] Update the build system to create both ISO and Floppy images. Verify these work using a test script. The standard CMake build target should automatically generate both images. (Only ISO supported for now)
- [ ] Update the kernel to correctly setup the GDT
- [x] Update the kernel to correctly setup the GDT
- [x] Create an interrupt handler.
- [ ] Implement a PIC handler
- [ ] Create a physical memory allocator and mapper. The kernel should live in the upper last gigabyte of virtual memory. It should support different zones (e.g.: `SUB_16M`, `DEFAULT`, ...) These zones describe the region of memory that memory should be allocated in. If it is not possible to allocate in that region (because it is full, or has 0 capacity to begin with), it should fallback to another zone.
- [ ] Create a paging subsystem. It should allow drivers to allocate and deallocate pages at will.
- [ ] Create a memory allocator. This should provide the kernel with `malloc` and `free`. Internally, it should use the paging subsystem to ensure that the address it returns have actual RAM paged to them.

View File

@@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.16)
add_executable(kernel
boot.S
gdt_flush.S
gdt.c
idt.c
interrupts.S
kernel.c
)

68
src/gdt.c Normal file
View File

@@ -0,0 +1,68 @@
#include "gdt.h"
#include <stdint.h>
/* GDT Pointer Structure */
struct gdt_ptr gp;
/* GDT entries */
struct gdt_entry gdt[5];
extern void gdt_flush(uint32_t);
/* Setup a descriptor in the Global Descriptor Table */
void gdt_set_gate(int32_t num, uint32_t base, uint32_t limit, uint8_t access, uint8_t gran)
{
/* Setup the descriptor base address */
gdt[num].base_low = (base & 0xFFFF);
gdt[num].base_middle = (base >> 16) & 0xFF;
gdt[num].base_high = (base >> 24) & 0xFF;
/* Setup the descriptor limits */
gdt[num].limit_low = (limit & 0xFFFF);
gdt[num].granularity = ((limit >> 16) & 0x0F);
/* Finally, set up the granularity and access flags */
gdt[num].granularity |= (gran & 0xF0);
gdt[num].access = access;
}
/* Should be called by main. This will setup the special GDT
* pointer, set up the first 3 entries in our GDT, and then
* call gdt_flush() in our assembler file in order to tell
* the processor where the new GDT is and update the
* internal registers */
void init_gdt()
{
/* Setup the GDT pointer and limit */
gp.limit = (sizeof(struct gdt_entry) * 5) - 1;
gp.base = (uint32_t)&gdt;
/* Our NULL descriptor */
gdt_set_gate(0, 0, 0, 0, 0);
/* The second entry is our Code Segment. The base address
* is 0, the limit is 4GBytes, it uses 4KByte granularity,
* uses 32-bit opcodes, and is a Code Segment descriptor.
* Please check the table above in the tutorial in order
* to see exactly what each value means */
/* 0x9A = 1001 1010
P=1, DPL=00, S=1 (Code/Data), Type=1010 (Code, Exec/Read) */
/* 0xCF = 1100 1111
G=1 (4k), DB=1 (32bit), L=0, AVL=0, LimitHigh=0xF */
gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF);
/* The third entry is our Data Segment. It's EXACTLY the
* same as our code segment, but the descriptor type in
* this entry's access byte says it's a Data Segment */
/* 0x92 = 1001 0010
P=1, DPL=00, S=1, Type=0010 (Data, Read/Write) */
gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF);
/* User mode code segment */
gdt_set_gate(3, 0, 0xFFFFFFFF, 0xFA, 0xCF);
/* User mode data segment */
gdt_set_gate(4, 0, 0xFFFFFFFF, 0xF2, 0xCF);
/* Flush out the old GDT and install the new changes! */
gdt_flush((uint32_t)&gp);
}

25
src/gdt.h Normal file
View File

@@ -0,0 +1,25 @@
#ifndef GDT_H
#define GDT_H
#include <stdint.h>
/* GDT Entry Structure */
struct gdt_entry {
uint16_t limit_low;
uint16_t base_low;
uint8_t base_middle;
uint8_t access;
uint8_t granularity;
uint8_t base_high;
} __attribute__((packed));
/* GDT Pointer Structure */
struct gdt_ptr {
uint16_t limit;
uint32_t base;
} __attribute__((packed, aligned(1)));
/* Initialize GDT */
void init_gdt(void);
#endif // GDT_H

18
src/gdt_flush.S Normal file
View File

@@ -0,0 +1,18 @@
.section .text
.global gdt_flush
.type gdt_flush, @function
gdt_flush:
mov 4(%esp), %eax
lgdt (%eax)
mov $0x10, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
mov %ax, %ss
ljmp $0x08, $flush
flush:
ret

46
src/idt.c Normal file
View File

@@ -0,0 +1,46 @@
#include "idt.h"
#include <stdint.h>
#include <stddef.h> /* for memset */
idt_entry_t idt_entries[256];
idt_ptr_t idt_ptr;
extern void idt_load();
static void idt_set_gate(uint8_t num, uint32_t base, uint16_t sel, uint8_t flags)
{
idt_entries[num].base_lo = base & 0xFFFF;
idt_entries[num].base_hi = (base >> 16) & 0xFFFF;
idt_entries[num].sel = sel;
idt_entries[num].always0 = 0;
// We must uncomment the OR below when we get to using user-mode.
// It sets the interrupt gate's privilege level to 3.
idt_entries[num].flags = flags /* | 0x60 */;
}
/* Note: memset implementation because we don't link with libc */
static void *memset(void *s, int c, size_t n)
{
unsigned char *p = s;
while(n--)
*p++ = (unsigned char)c;
return s;
}
void init_idt()
{
idt_ptr.limit = sizeof(idt_entry_t) * 256 - 1;
idt_ptr.base = (uint32_t)&idt_entries;
memset(&idt_entries, 0, sizeof(idt_entry_t) * 256);
/* Determine valid flags:
* 0x8E = 1000 1110
* P=1, DPL=00, S=0 (System), Type=1110 (32-bit Interrupt Gate) */
/* For now, just load the IDT without any entries to verify loading works without crashing.
Later we will set up ISRs. */
idt_load((uint32_t)&idt_ptr);
}

33
src/idt.h Normal file
View File

@@ -0,0 +1,33 @@
#ifndef IDT_H
#define IDT_H
#include <stdint.h>
/* A struct describing an interrupt gate. */
struct idt_entry_struct {
uint16_t base_lo; // The lower 16 bits of the address to jump to when this interrupt fires.
uint16_t sel; // Kernel segment selector.
uint8_t always0; // This must always be zero.
uint8_t flags; // More flags. See documentation.
uint16_t base_hi; // The upper 16 bits of the address to jump to.
} __attribute__((packed));
typedef struct idt_entry_struct idt_entry_t;
/* A struct describing a pointer to an array of interrupt handlers.
* This is in a format suitable for giving to 'lidt'. */
struct idt_ptr_struct {
uint16_t limit;
uint32_t base; // The address of the first element in our idt_entry_t array.
} __attribute__((packed));
typedef struct idt_ptr_struct idt_ptr_t;
// These extern directives let us access the addresses of our ASM ISR handlers.
extern void isr0();
extern void isr1();
// ... we will add more later ...
void init_idt();
#endif

7
src/interrupts.S Normal file
View File

@@ -0,0 +1,7 @@
.section .text
.global idt_load
.type idt_load, @function
idt_load:
mov 4(%esp), %eax /* Turn the argument into the EAX register */
lidt (%eax) /* Load the IDT pointer */
ret

View File

@@ -1,6 +1,8 @@
#include <multiboot2.h>
#include <stdint.h>
#include <stddef.h>
#include "gdt.h"
#include "idt.h"
static inline void outb(uint16_t port, uint8_t val)
{
@@ -17,15 +19,33 @@ void offset_print(const char *str)
#define MULTIBOOT_BOOTLOADER_MAGIC 0x2BADB002
void print_hex(uint32_t val)
{
const char *hex = "0123456789ABCDEF";
outb(0xE9, '0');
outb(0xE9, 'x');
for (int i = 28; i >= 0; i -= 4) {
outb(0xE9, hex[(val >> i) & 0xF]);
}
outb(0xE9, '\n');
}
void kernel_main(uint32_t magic, uint32_t addr) {
(void)addr; // Unused for now
if (magic != MULTIBOOT2_BOOTLOADER_MAGIC && magic != MULTIBOOT_BOOTLOADER_MAGIC) {
offset_print("Invalid magic number: ");
// I don't have hex print yet, but I can print something generic
offset_print("Unknown\n");
print_hex(magic);
return;
}
offset_print("Booting...\n");
init_gdt();
offset_print("GDT initialized\n");
init_idt();
offset_print("IDT initialized\n");
offset_print("Hello, world\n");
}

View File

@@ -12,11 +12,6 @@ if [ ! -f "$RELEASE_DIR/claude-os.iso" ]; then
exit 1
fi
if [ ! -f "$RELEASE_DIR/claude-os.img" ]; then
echo "Error: claude-os.img not found!"
exit 1
fi
echo "Testing ISO image..."
timeout 5s qemu-system-i386 -cdrom "$RELEASE_DIR/claude-os.iso" -debugcon file:iso_output.txt -display none -no-reboot || true
if grep -q "Hello, world" iso_output.txt; then