I finally learned how to learn OSDev

All off topic discussions go here. Everything from the funny thing your cat did to your favorite tv shows. Non-programming computer questions are ok too.
Post Reply
vinny
Posts: 6
Joined: Sun Oct 22, 2023 6:28 pm

I finally learned how to learn OSDev

Post by vinny »

Hello!

I've dipped my toes in OSDev before.
I'm a seasoned C/C++ programmer, understand the basics of assembly, understand data types, logic, etc.

Previously, when tackling OSDev, I'd just copy-paste Meaty Skeleton, copy-paste someone else's GDTs, IDTs, ISRs, and then I'd have a little shell that would do nothing in about 20 minutes.
However, I would have no idea how any of it worked, or how to add anything new.

I've realized that that is stupid, and that I can do better.
So, I read every dang Wiki link that I needed, understood it, and began programming a GDT on top of Meaty Skeleton, and BOY is it taking me a minute.

Sure, I could've just hard-coded it in about 5 minutes, and then programmed a stub to load it.
Instead, I took the time to write a bunch of C code, chock full of enums and structs, to make a very versatile and robust GDT generator, to fully understand how it works.

This is working very well so far, because now I actually KNOW what my code does, instead of a rough guess.

Anyway, here it is. Let me know if there is anything I can do to improve it!
(Node, I haven't written the loader, just the structs and stuff that I will use for it)

Code: Select all

#ifndef _ARCH_INIT_GDT_INIT_H
#define _ARCH_INIT_GDT_INIT_H

#include <stdint.h>
#include <stddef.h>

typedef struct{
    uint32_t base;
    uint32_t limit;
    uint8_t access_byte;
    uint8_t flags;
} gdt_entry_t;

typedef enum {
    GDT_FLAG_GRANULARITY_4K =   (1 << 7), // Segment limit is in 4KB blocks
    GDT_FLAG_GRANULARITY_BYTE = (0 << 7), // Segment limit is in bytes
    GDT_FLAG_SIZE_32 =          (1 << 6), // For 32-bit code segments
    GDT_FLAG_SIZE_16 =          (0 << 6), // For 16-bit code segments

    // Note: GDT_FLAG_SIZE_32 (D/B bit) must be cleared (0) if GDT_FLAG_SIZE_64 (L bit) is set.
    GDT_FLAG_LONG_MODE_64 =     (1 << 5), // For 64-bit code segments
    GDT_FLAG_LONG_MODE_32 =     (0 << 5), // For 32-bit or 16-bit segments
} gdt_flags_high;

typedef enum {
    GDT_ACCESS_PRESENT        = (1 << 7), // Segment is present. Must be 1

    GDT_ACCESS_RING0          = (0 << 5), // Ring 0, Kernel
    GDT_ACCESS_RING1          = (1 << 5),
    GDT_ACCESS_RING2          = (2 << 5),
    GDT_ACCESS_RING3          = (3 << 5), // Ring 1, User

    GDT_ACCESS_SYSTEM         = (0 << 4), // Cleared if segment is system (for TSS or LDT. Use gdt_segment_system_type)
    GDT_ACCESS_CODE_DATA      = (1 << 4), // Set if segment is code or data

    GDT_ACCESS_DATA           = (0 << 3), // Segment is non executable
    GDT_ACCESS_CODE           = (1 << 3), // Segment is executable

    GDT_ACCESS_DIR_UP         = (0 << 2), // Data: Segment grows up.
    GDT_ACCESS_DIR_DOWN       = (1 << 2), // Data: Segment grows down. Offset has to be greater than the limit.

    GDT_ACCESS_CONFORMING     = (0 << 2), // Code: This segment can only be executed from the ring set in DPL.
    GDT_ACCESS_NON_CONFORMING = (1 << 2), // Code: This segment can be executed from an equal or lower privilege level. 

    GDT_ACCESS_CODE_READABLE  = (1 << 1), // Code: This segment is readable. Write access is never allowed for code segments.
    
    GDT_ACCESS_DATA_WRITABLE  = (1 << 1), // Data: This segment is writable. Read access is always allowed for data segments.

    GDT_ACCESS_ACCESSED       = (0 << 1), // The CPU will set it when the segment is accessed unless set to 1 in advance. This means that in case the GDT descriptor is stored in read only pages and this bit is set to 0, the CPU trying to set this bit will trigger a page fault. Best left set to 1 unless otherwise needed.
                                     
} gdt_segment_access_byte;

typedef enum {
    // Protected mode
    GDT_SYSTEM_TSS_16_AVALIABLE      = 1,
    GDT_SYSTEM_LDT                   = 2,
    GDT_SYSTEM_TSS_16_BUSY           = 3,
    GDT_SYSTEM_TSS_32_AVALIABLE      = 9,
    GDT_SYSTEM_TSS_32_BUSY           = 11,

    // Long mode
    GDT_SYSTEM_LONG_LDT              = 2,
    GDT_SYSTEM_LONG_TSS_64_AVALIABLE = 9,
    GDT_SYSTEM_LONG_TSS_64_BUSY      = 11,
} gdt_segment_system_type;

#endif

Code: Select all

#include <arch/i386/init/gdt_init.h>

// GDT ENTRIES
const gdt_entry_t entries[] = {
    // Null Segment
    {
        .base = 0,
        .limit = 0,
        .access_byte = 0,
        .flags = 0,
    },

    // Kernel Code Segment
    {
        .base = 0x00400000,
        .limit = 0xFFFFF,
        .access_byte = (
            GDT_ACCESS_PRESENT       |  // P flag
            GDT_ACCESS_RING0         |  // DPL flags
            GDT_ACCESS_CODE_DATA     |  // S flag
            GDT_ACCESS_CODE          |  // E flag
            GDT_ACCESS_CONFORMING    |  // DC flag
            GDT_ACCESS_CODE_READABLE    // RW flag
        ),
        .flags = (
            GDT_FLAG_GRANULARITY_4K  |  // G flag
            GDT_FLAG_SIZE_32            // DB flag
        )
    },

    // Kernel Data Segment
    {
        .base = 0x00400000,
        .limit = 0xFFFFF,
        .access_byte = (
            GDT_ACCESS_PRESENT       |  // P flag
            GDT_ACCESS_RING0         |  // DPL flags
            GDT_ACCESS_CODE_DATA     |  // S flag
            GDT_ACCESS_DATA          |  // E flag
            GDT_ACCESS_DIR_UP        |  // DC flag
            GDT_ACCESS_DATA_WRITABLE    // RW flag
        ),
        .flags = (
            GDT_FLAG_GRANULARITY_4K  |
            GDT_FLAG_SIZE_32
        )
    }
};

void encode_gdt_entry(uint8_t *target, gdt_entry_t source)
{
    // Check the limit to make sure that it can be encoded
    if (source.limit > 0xFFFFF) {/*kerror("GDT cannot encode limits larger than 0xFFFFF");*/ return; }
    
    // Encode the limit
    target[0] = source.limit & 0xFF;
    target[1] = (source.limit >> 8) & 0xFF;
    target[6] = (source.limit >> 16) & 0x0F;
    
    // Encode the base
    target[2] = source.base & 0xFF;
    target[3] = (source.base >> 8) & 0xFF;
    target[4] = (source.base >> 16) & 0xFF;
    target[7] = (source.base >> 24) & 0xFF;
    
    // Encode the access byte
    target[5] = source.access_byte;
    
    // Encode the flags (They should be in the high 4 bits already)
    target[6] |= (source.flags);
}
Octocontrabass
Member
Member
Posts: 6245
Joined: Mon Mar 25, 2013 7:01 pm

Re: I finally learned how to learn OSDev

Post by Octocontrabass »

vinny wrote: Wed Jul 30, 2025 11:46 amAnyway, here it is. Let me know if there is anything I can do to improve it!
I'd recommend setting the base to 0 in your code and data segments.

Good work taking the time to learn about the GDT, but it seems a bit over-engineered for a typical OS that will never change any of its code or data segment descriptors.
vinny
Posts: 6
Joined: Sun Oct 22, 2023 6:28 pm

Re: I finally learned how to learn OSDev

Post by vinny »

Octocontrabass wrote: Wed Jul 30, 2025 10:05 pm Good work taking the time to learn about the GDT, but it seems a bit over-engineered for a typical OS that will never change any of its code or data segment descriptors.
True, true, honestly, I could've hard-coded it and it would've been fine. But the main reason for this is education / hobby, as most home-brewed OSes are. I also have a bad habit of just copy-pasting mindlessly without learning, so I made this requirement for myself so I can actually understand it!
Post Reply