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);
}
