Making the GDT
Before we move into protected mode we must create the GDT. To have a complete GDT we first need 3 descriptors, one is the null descriptor which is just 64 bits of 0. The second is the kernel space code descriptor. The third is the kernel space data descriptor. We could also make descriptors for the user space, but I will refrain from doing that for now as making the user space will come much later on.
First let's define the null descriptor, which looks like this:
gdt_start:
gdt_null:
dq 0
Pretty simple, now for the code descriptor
gdt_code:
dw 0xFFFF ; limit
dw 0x0000 ; base_low
db 0x00 ;base_middle
db 0x9A ;access
db 0xCF ;flags + limit high 4 bits
db 0x00 ;base_high
The base:
The comments I left here are pretty useful as they describe descriptor structure.
We set all the Base
to 0 because we want the starting address for our kernel space code to
be address 0. This is because with our descriptor we are essentially
making a flat memory model so whenever we reference an address. For
example writing to 0xB8000 (for VGA output), would be accessed
via the base + offset (0 + 0xB8000). It makes it so we don't have to factor in a
base to get to the addresses we want.
The limit:
The limit tells us how much memory the segment is allowed to access. We set it all to F because this is our kernel, so we want it to be able to access everything that exists in memory.
The access byte
The access byte tells us what kind of segment we have and who has permission
to use it. For our 0x9A this translates to 0x9A = 10011010, let's look
at the flags:
P DPL DPL S E DC RW A
1 0 0 1 1 0 1 0
- The Present flag (P) tells us that this segment exists, if it's set to 0, and we try to access the segment, the CPU will fault.
- The Descriptor Privilege Level (DPL) is self-explanatory, we set it to 0 because we want the highest privilege.
- The Descriptor Type (S) states what kind of descriptor it is, if
this is set to 1 it's a normal code or data segment, if 0, it's a
system descriptor such as a
LDT. We don't need that yet - The Executable (E) when set to 1 means that it's a code segment, when zero it represents data
- DC stands for Direction/Conforming When set to 0 it's a non-conforming code segment, this means that only code running at the correct privilege level may enter it, if DC was 1 then code running at the same or a lower privilege level may enter the segment
- Read/Write (RW) when set to 0 means executable only. We should set RW to 1 so it can be executable and readable
- The access bit (A) states whether the descriptor has been accessed. The CPU automatically sets this bit when the segment is accessed.
We can see that the access byte, as the name implies, controls access.
The flags nybble
Our 0xC = 1100, let's take a look:
G D L AVL
1 1 0 0
- Granularity (G) being set to 1 makes our limit get measured in 4KiB blocks rather than bytes
- Default Operand Size (D) being set to one states the segment is 32 bits instead of 16
- Long mode (L) being set to 0 keeps us in 32 bits, if it's 1 it indicates a 64 bit code segment in long mode
- The Available (AVL) flag is ignored by the CPU, so let's just set it to 0
Now let's look at the data segment which is pretty similar:
gdt_data:
dw 0xFFFF
dw 0x0000
db 0x00
db 0x92
db 0xCF
db 0x00
The only difference here is that we turn off the executable flag as this is a data segment and not a code segment.
The final part of the GDT is data that we will load into the global descriptor table register. This will include the size of the GDT and the start address. Mine looks like this:
gdt_end:
gdtr:
dw gdt_end - gdt_start - 1 ; set manually for testing
dd gdt_start
Going into Protected mode
And that's the end of our GDT and all the data we need. Now we can start entering the GDT, we can do that with this block of instructions:
enter_protected:
cli ;disable interrupts
lgdt [gdtr] ; load GDT registor with start address of GDT
mov eax, cr0
or eax, 1 ;set protection enable bit in control register 0 (cr0)
mov cr0, eax
CODE_SEG equ gdt_code - gdt_start
jmp CODE_SEG:p_mode_main
This is essentially 3 things. We first use cli which disables maskable
hardware interrupts. We then load the descriptor table with lgdt, and then we set
the protection enable bit in the control register, After this we then
perform a far jump into the p_mode_main label (which we will define
later) using the code descriptor.
p_mode_main:
mov ax, 10h
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, 0x9000
hang:
jmp hang
This is our code for our p_mode_main. We just set the data
segment registers. If we wanted, we could replace CODE_SEG with 08h,
as this is the value calculated by gdt_code - gdt_start. We then load
the data segment selector, 10h, into our other segment registers.
And that should be all for going into protected mode, our full code should look like this:
; no org code starts at 0x9000 though
[bits 16]
start:
mov ax, cs
mov ds, ax
mov si, hello_string
call print_string
jmp enter_protected
print_string:
mov ah, 0Eh
print_char:
lodsb ; sets al = [DS:SI++]
cmp al, 0
je done
int 10h
jmp print_char
done:
ret
enter_protected:
cli ;disable interrupts
lgdt [gdtr] ; load GDT registor with start address of GDT
mov eax, cr0
or eax, 1 ;set protection enable bit in control register 0 (cr0)
mov cr0, eax
CODE_SEG equ gdt_code - gdt_start
jmp CODE_SEG:p_mode_main
[bits 32]
p_mode_main:
mov ax, 10h
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, 0x9000
hang:
jmp hang
hello_string db 'Hello World!, i am lytlnyblOS, running in real mode', 0
gdt_start:
gdt_null:
dq 0
gdt_code:
dw 0xFFFF ; limit
dw 0x0000 ; base_low
db 0x00 ;base_middle
db 0x9A ;access
db 0xCF ;flags + limit high 4 bits
db 0x00 ;base_high
gdt_data:
dw 0xFFFF
dw 0x0000
db 0x00
db 0x92
db 0xCF
db 0x00
gdt_end:
gdtr:
dw gdt_end - gdt_start - 1 ; set manually for testing
dd gdt_start
Let's take away our -s and -S flags from our Makefile and then run the program and see what happens. What is most likely happening for you is that it looks like a bunch of text is flashing on the screen. This is probably our triple fault, if not and the program hangs, you're probably in real mode and can skip what I'm about to talk about next.