For More info on the GDT Visit:
1 gdt.c
:
#include <system.h>
/*
* Global Descriptor Table (GDT) Entry
* Each entry in the GDT is 8 bytes long.
*/
struct gdt_entry {
unsigned short limit_low; // The lower 16 bits of the limit.
unsigned short base_low; // The lower 16 bits of the base address.
unsigned char base_middle; // The next 8 bits of the base address.
unsigned char access; // Access flags, determine what ring this segment can be used in.
unsigned char granularity; // Granularity, and upper 4 bits of the limit.
unsigned char base_high; // The last 8 bits of the base address.
} __attribute__((packed));
/*
* GDT pointer structure
* Points to the start of our array of GDT entries
* and the size of the array (limit).
*/
struct gdt_ptr {
unsigned short limit; // The upper 16 bits of all selector limits.
unsigned int base; // The address of the first gdt_entry struct.
} __attribute__((packed));
struct gdt_entry gdt[3]; // Define 3 entries in our GDT.
struct gdt_ptr gp; // Define the pointer to the GDT.
/*
* (ASM) gdt_flush
* This will be defined in assembly, it reloads the segment registers.
*/
extern void gdt_flush();
/*
* gdt_set_gate
* Set a GDT descriptor.
* @param num: The index of the GDT entry to set.
* @param base: The base address of the segment.
* @param limit: The limit of the segment.
* @param access: The access flags for the segment.
* @param gran: The granularity flags for the segment.
*/
void gdt_set_gate(int num, unsigned long base, unsigned long limit, unsigned char access, unsigned char gran) {
/* Base Address */
gdt[num].base_low = (base & 0xFFFF); // Set the lower 16 bits of the base.
gdt[num].base_middle = (base >> 16) & 0xFF; // Set the next 8 bits of the base.
gdt[num].base_high = (base >> 24) & 0xFF; // Set the high 8 bits of the base.
/* Limits */
gdt[num].limit_low = (limit & 0xFFFF); // Set the lower 16 bits of the limit.
gdt[num].granularity = (limit >> 16) & 0x0F; // Set the high 4 bits of the limit in granularity.
/* Granularity */
gdt[num].granularity |= (gran & 0xF0); // Combine the granularity flags.
/* Access flags */
gdt[num].access = access; // Set the access flags.
}
/*
* gdt_install
* Install the kernel's GDT.
*/
void gdt_install() {
/* GDT pointer and limits */
gp.limit = (sizeof(struct gdt_entry) * 3) - 1; // Calculate the total size of the GDT.
// Set the base address of the GDT
// Without cast: some compilers may issue a warning or error.
// gp.base = &gdt;
// With explicit cast: ensures the address is treated as an integer.
gp.base = (unsigned int)&gdt; // Set the base address of the GDT.
/* NULL segment: required by x86 architecture */
gdt_set_gate(0, 0, 0, 0, 0); // Set the first GDT entry (null descriptor).
/* Code segment: base=0, limit=4GB, accessed from ring 0 */
gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF); // Set the code segment descriptor.
/* Data segment: base=0, limit=4GB, accessed from ring 0 */
gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF); // Set the data segment descriptor.
/* Load the new GDT using the gdt_flush function */
gdt_flush();
}
Code Explanation:
Structures for GDT Entries and GDT Pointer
GDT Entry Structure (gdt_entry)
:
/*
* Global Descriptor Table (GDT) Entry
* Each entry in the GDT is 8 bytes long.
*/
struct gdt_entry {
unsigned short limit_low; // The lower 16 bits of the limit.
unsigned short base_low; // The lower 16 bits of the base address.
unsigned char base_middle; // The next 8 bits of the base address.
unsigned char access; // Access flags, determine what ring this segment can be used in.
unsigned char granularity; // Granularity, and upper 4 bits of the limit.
unsigned char base_high; // The last 8 bits of the base address.
} __attribute__((packed));
This structure represents a single entry in the GDT. It includes:
- limit_low: The lower 16 bits of the segment limit.
- base_low: The lower 16 bits of the base address.
- base_middle: The next 8 bits of the base address.
- access: The access flags (e.g., segment type, privilege level).
- granularity: The granularity and size flags, along with the upper 4 bits of the segment limit.
- base_high: The upper 8 bits of the base address.
The __attribute__((packed))
ensures that the compiler does not add any padding between the members of the structure.
GDT Pointer Structure (gdt_ptr)
:
/*
* GDT pointer structure
* Points to the start of our array of GDT entries
* and the size of the array (limit).
*/
struct gdt_ptr {
unsigned short limit; // The upper 16 bits of all selector limits.
unsigned int base; // The address of the first gdt_entry struct.
} __attribute__((packed));
This structure holds the address and size of the GDT. The CPU needs this information to load the GDT.
- limit: The size of the GDT minus one.
- base: The base address of the first GDT entry.
Declaration of GDT and GDT Pointer
Global Declarations
:
struct gdt_entry gdt[3]; // Define 3 entries in our GDT.
struct gdt_ptr gp; // Define the pointer to the GDT.
- gdt: An array of three GDT entries.
- gp: The GDT pointer.
External Function Declaration
GDT Flush Function
:
/*
* (ASM) gdt_flush
* This will be defined in assembly, it reloads the segment registers.
*/
extern void gdt_flush();
This external function (written in assembly) reloads the segment registers to use the new GDT. The actual implementation of gdt_flush
is not provided here, but it typically involves loading the new GDT pointer into the GDTR register.
Setting a GDT Entry
gdt_set_gate
Function:
/*
* gdt_set_gate
* Set a GDT descriptor.
* @param num: The index of the GDT entry to set.
* @param base: The base address of the segment.
* @param limit: The limit of the segment.
* @param access: The access flags for the segment.
* @param gran: The granularity flags for the segment.
*/
void gdt_set_gate(int num, unsigned long base, unsigned long limit, unsigned char access, unsigned char gran) {
/* Base Address */
gdt[num].base_low = (base & 0xFFFF); // Set the lower 16 bits of the base.
gdt[num].base_middle = (base >> 16) & 0xFF; // Set the next 8 bits of the base.
gdt[num].base_high = (base >> 24) & 0xFF; // Set the high 8 bits of the base.
/* Limits */
gdt[num].limit_low = (limit & 0xFFFF); // Set the lower 16 bits of the limit.
gdt[num].granularity = (limit >> 16) & 0x0F; // Set the high 4 bits of the limit in granularity.
/* Granularity */
gdt[num].granularity |= (gran & 0xF0); // Combine the granularity flags.
/* Access flags */
gdt[num].access = access; // Set the access flags.
}
This function initializes a single GDT entry:
- base: The base address of the segment.
- limit: The size of the segment.
- access: The access flags (e.g., segment type, privilege level).
- gran: The granularity and size flags.
Installing the GDT
gdt_install
Function:
/*
* gdt_install
* Install the kernel's GDT.
*/
void gdt_install() {
/* GDT pointer and limits */
gp.limit = (sizeof(struct gdt_entry) * 3) - 1; // Calculate the total size of the GDT.
// Set the base address of the GDT
// Without cast: some compilers may issue a warning or error.
// gp.base = &gdt;
// With explicit cast: ensures the address is treated as an integer.
gp.base = (unsigned int)&gdt; // Set the base address of the GDT.
/* NULL segment: required by x86 architecture */
gdt_set_gate(0, 0, 0, 0, 0); // Set the first GDT entry (null descriptor).
/* Code segment: base=0, limit=4GB, accessed from ring 0 */
gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF); // Set the code segment descriptor.
/* Data segment: base=0, limit=4GB, accessed from ring 0 */
gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF); // Set the data segment descriptor.
/* Load the new GDT using the gdt_flush function */
gdt_flush();
}
This function sets up the GDT:
- Initialize GDT Pointer: Set the limit and base address of the GDT.
- Set GDT Entries:
- Null Segment (Index 0): This is a mandatory entry that acts as a placeholder and is not used.
- Code Segment (Index 1): Defines a code segment with read and execute permissions, spanning the entire 4 GB address space.
- Data Segment (Index 2): Defines a data segment with read and write permissions, also spanning the entire 4 GB address space.
- Flush the GDT: Call
gdt_flush
which is an external assembly function to load the new GDT and update the segment registers.
2 gdt_asm.asm
This file would contain the definition of gdt_flush
, we can define it in the already existing file start.asm
however doing so make our code unmaintainable later on. So we go with a new file for the definition of it.
; Declare that the gdt_flush function is global, so it can be used outside this file.
global gdt_flush
; Declare that the gp (GDT pointer) is defined in another file.
extern gp
; Define the gdt_flush function.
gdt_flush:
; Load the GDT pointer (gp) into the GDTR register using the lgdt instruction.
lgdt [gp]
; Set up segment registers with the data segment selector.
; 0x10 is the selector for the data segment, which is the second entry in the GDT.
mov ax, 0x10
mov ds, ax ; Load the data segment (ds) register.
mov es, ax ; Load the extra segment (es) register.
mov fs, ax ; Load the fs segment register.
mov gs, ax ; Load the gs segment register.
mov ss, ax ; Load the stack segment (ss) register.
; Perform a far jump to reload the code segment register (cs) with the code segment selector.
; 0x08 is the selector for the code segment, which is the first entry in the GDT.
jmp 0x08:flush2
; Label for the code that is executed after the far jump.
flush2:
; Return from the function.
ret
Assembly Code Explanation
1 Global and External Declarations
global gdt_flush
extern gp
global gdt_flush
: This makes thegdt_flush
label accessible from other files, allowing the C code to call this function.extern gp
: This declares thatgp
(the GDT pointer) is defined in another file (the C code in this case). The assembly code will referencegp
to load the GDT.
2 gdt_flush
Function:
gdt_flush:
- This label marks the start of the
gdt_flush
function.
lgdt [gp]
lgdt [gp]
: This instruction loads the GDT descriptor from the memory location pointed to bygp
. The GDT descriptor contains the base address and limit of the GDT.
Reload Segment Registers
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov ax, 0x10
: Load the value0x10
into theax
register. The value0x10
corresponds to the selector for the data segment in the GDT (second entry, with an index of 2, shifted left by 3 bits).mov ds, ax
: Load theds
(Data Segment) register with the value inax
.mov es, ax
: Load thees
(Extra Segment) register with the value inax
.mov fs, ax
: Load thefs
(Extra Segment) register with the value inax
.mov gs, ax
: Load thegs
(Extra Segment) register with the value inax
.mov ss, ax
: Load thess
(Stack Segment) register with the value inax
.
These instructions reload all the segment registers (ds
, es
, fs
, gs
, ss
) with the data segment selector (0x10
). This step is necessary to ensure that the segment registers use the newly defined GDT entries.
Far Jump to Flush Instruction Pipeline
jmp 0x08:flush2
jmp 0x08:flush2
: This is a far jump that reloads thecs
(Code Segment) register with the value0x08
(selector for the code segment in the GDT) and jumps to theflush2
label. This instruction ensures that thecs
register is updated with the new GDT entry for the code segment.
Return from Function
flush2:
ret
flush2
: This label marks the target for the far jump.ret
: This instruction returns control to the calling function in the C code.
3 Call gdt_install()
from Kernel Main
Since we did all the setup of the gdt table in the gdt_install()
function. Now it's time to call this from our kernel main()
function.
Modify the main.c
file to add the function call to gdt_install()
:
int
main() {
..........................
gdt_install();
..........................
init_video();
puts("Hello world!\n");
for (;;);
and need to add the extern declaration to the system.h
file as well. So that
/* GDT */
extern void gdt_install();
extern void gdt_set_gate(int num, unsigned long base, unsigned long limit, unsigned char access, unsigned char gran);
3 Modify Makefile
Since we introduced a c
and asm
file, now need to modify the makefile to add rule for their compilation.
.PHONY: all clean run install
# Compiler and assembler flags
CC = gcc
CFLAGS = -Wall -m32 -fno-pie -O0 -fstrength-reduce -fomit-frame-pointer -finline-functions -nostdinc -fno-builtin -I./include
AS = nasm
ASFLAGS = -f elf
# Output files and directories
ISO_DIR = iso
BOOT_DIR = $(ISO_DIR)/boot
GRUB_DIR = $(BOOT_DIR)/grub
ISO = my_os.iso
# Source files and objects
C_SOURCES = main.c vga.c gdt.c
ASM_SOURCES = start.asm gdt_asm.asm
OBJ = $(C_SOURCES:.c=.o) $(ASM_SOURCES:.asm=.o)
# Linker script
LINKER_SCRIPT = link.ld
# Kernel binary
KERNEL = kernel.bin
all: $(KERNEL)
# Build the ISO and run it with QEMU
run: install
qemu-system-x86_64 -cdrom $(ISO)
# Install the kernel and GRUB configuration into the ISO directory and create the ISO image
install: $(KERNEL)
mkdir -p $(GRUB_DIR)
cp $(KERNEL) $(BOOT_DIR)/kernel.bin
cp grub.cfg $(GRUB_DIR)/grub.cfg
grub-mkrescue -o $(ISO) $(ISO_DIR)
# Link the kernel binary
$(KERNEL): $(OBJ) $(LINKER_SCRIPT)
ld -m elf_i386 -T $(LINKER_SCRIPT) -o $(KERNEL) $(OBJ)
# Compile C source files
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
# Assemble assembly source files
%.o: %.asm
$(AS) $(ASFLAGS) -o $@ $<
# Clean up build artifacts
clean:
rm -f $(OBJ) $(KERNEL) $(ISO)
rm -rf $(ISO_DIR)
# Dependencies
main.o: main.c
vga.o: vga.c
gdt.o: gdt.c
start.o: start.asm
gdt_asm.o: gdt_asm.asm
1 Phony Targets:
.PHONY: all clean run install
all
,clean
,run
, andinstall
are declared as phony targets, meaning they don't correspond to actual files but are just names for commands to run.
2 Variables:
# Compiler and assembler flags
CC = gcc
CFLAGS = -Wall -m32 -fno-pie -O0 -fstrength-reduce -fomit-frame-pointer -finline-functions -nostdinc -fno-builtin -I./include
AS = nasm
ASFLAGS = -f elf
CC
andAS
are the compiler and assembler, respectively.CFLAGS
are the compiler flags for GCC, specifying options like 32-bit mode (-m32
), no position-independent code (-fno-pie
), and optimization level (-O0
).ASFLAGS
are the assembler flags for NASM, specifying ELF format (-f elf
).
3 Directories and Files:
# Output files and directories
ISO_DIR = iso
BOOT_DIR = $(ISO_DIR)/boot
GRUB_DIR = $(BOOT_DIR)/grub
ISO = my_os.iso
ISO_DIR
,BOOT_DIR
, andGRUB_DIR
are directories for organizing the ISO structure.ISO
is the name of the ISO image to be created.
# Source files and objects
C_SOURCES = main.c vga.c gdt.c
ASM_SOURCES = start.asm gdt_asm.asm
OBJ = $(C_SOURCES:.c=.o) $(ASM_SOURCES:.asm=.o)
C_SOURCES
andASM_SOURCES
list the source files.OBJ
lists the object files generated from these source files.
# Linker script
LINKER_SCRIPT = link.ld
# Kernel binary
KERNEL = kernel.bin
LINKER_SCRIPT
is the linker script used by the linker to generate the final binary.KERNEL
is the output kernel binary.
4 Targets and Rules:
all: $(KERNEL)
- The
all
target depends on the$(KERNEL)
file.
run: install
qemu-system-x86_64 -cdrom $(ISO)
- The
run
target depends on theinstall
target and runs the ISO using QEMU.
install: $(KERNEL)
mkdir -p $(GRUB_DIR)
cp $(KERNEL) $(BOOT_DIR)/kernel.bin
cp grub.cfg $(GRUB_DIR)/grub.cfg
grub-mkrescue -o $(ISO) $(ISO_DIR)
- The
install
target depends on the$(KERNEL)
file, creates necessary directories, copies the kernel and GRUB configuration, and creates the ISO image usinggrub-mkrescue
.
$(KERNEL): $(OBJ) $(LINKER_SCRIPT)
ld -m elf_i386 -T $(LINKER_SCRIPT) -o $(KERNEL) $(OBJ)
- The
$(KERNEL)
target depends on the object files and the linker script, and links them into the final kernel binary.
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
- This pattern rule compiles C source files into object files.
%.o: %.asm
$(AS) $(ASFLAGS) -o $@ $<
- This pattern rule assembles assembly source files into object files.
clean:
rm -f $(OBJ) $(KERNEL) $(ISO)
rm -rf $(ISO_DIR)
- The
clean
target removes all generated files and directories to clean the build environment.
# Dependencies
main.o: main.c
vga.o: vga.c
gdt.o: gdt.c
start.o: start.asm
gdt_asm.o: gdt_asm.asm
- These explicit rules specify dependencies for the object files, ensuring that changes to source files trigger recompilation.