You are browsing as a guest. Sign up (or log in) to start making projects!

5h 0m 39s logged

First devlog where the screen looks exactly the same before and after.
Turns out that’s the interesting part: the GDT is the table the CPU reads on every single memory access, and doing it right means nothing visible happens.

What I built

  • A real VGA text driver (vga.c): putchar, write, line wrap, \n, and scrolling. The surprise: there is no hardware scroll in VGA text mode, scrolling means copying all 2000 cells up one row yourself and blanking the last one. I’d always assumed the terminal did that for you, it’s a for loop.

  • The GDT (gdt.c, gdt.h): three entries: a null one (mandatory, so an uninitialized segment register faults immediately instead of doing something random), one for code, one for data. Base 0 and limit 4GB on both, the flat model. You can’t turn segmentation off on x86, so you make it irrelevant instead.

  • Replacing GRUB’s table.: GRUB leaves you a working GDT, otherwise the kernel wouldn’t run at all, but it lives in GRUB’s memory and GRUB is gone. My stack grows, .bss expands, an allocator shows up eventually, and one day something writes over it. From that moment the CPU reads garbage on every memory access.

  • Shift and mask code for a 40 year old compatibility hack.: A descriptor is 8 bytes with the base address split into three non-contiguous chunks and the limit split in two. The 286 had 24 bit addresses in 1982, the 386 needed 8 more bits of base and 4 more of limit and couldn’t move any existing field without breaking every OS already written, so the new bits got wedged into the spare bytes at the end. __attribute__((packed)) isn’t optional here, one byte of padding and the CPU misreads everything.

  • The assembly loader (gdt_flush.s): lgdt plus reloading all five data segment registers, because each one caches the descriptor internally and changing the table doesn’t touch registers already loaded.

  • A jump that goes nowhere.: mov cs, ax does not exist: cs says which segment your code is in and eip says where in it, two halves of one address, so changing only cs lands the next instruction fetch at the old offset inside the new segment. They have to change together, and the only instruction that does that is a far jump. The idiom is jumping to the very next line. I disassembled it to convince myself: the jump sits at 0x15, is 7 bytes long, ends at 0x1c, and its destination is 0x1c. The jump is the vehicle, loading cs is the cargo.

  • Proof that something invisible happened. Nothing changed on screen so I asked the CPU through the QEMU monitor: GDT= 001042f4 00000017, and nm says my table is at 001042f4 with sizeof(gdt) - 1 = 23 = 0x17. CS reads [-R-], readable not writable, DS reads [-WA], writable not executable. I set the data access byte to 0x92 and the CPU reports 93: that’s the accessed bit, hardware turns it on by itself the first time the segment is used.

Look forward for the next steps

0
1

Comments 0

No comments yet. Be the first!