Crash after loading GDT and flushing registers

Question about which tools to use, bugs, the best way to implement a function, etc should go here. Don't forget to see if your question is answered in the wiki first! When in doubt post here.
YDeeps1
Member
Member
Posts: 69
Joined: Tue Aug 31, 2021 7:25 am
Discord: speedy.dev
Contact:

Crash after loading GDT and flushing registers

Post by YDeeps1 »

I'm still learning so I apologise if this issue is trivial.
I made my GDT in C++ which defines both the kernel and user segments, problem is with my asm (probably with my table). VirtualBox crashes after the:

Code: Select all

mov ss, ax
instruction.

gdt.h

Code: Select all

#pragma once
#include "stdint.h"

struct GDTDescriptor {
    uint16_t Size;
    uint32_t Offset;
} __attribute__((packed));

struct GDTEntry {
    uint16_t Limit0;
    uint16_t Base0;
    uint8_t Base1;
    uint8_t AccessByte;
    uint8_t Limit1_Flags;
    uint8_t Base2;
} __attribute__((packed));

struct GDT {
    GDTEntry Null;
    GDTEntry KernelCode;
    GDTEntry KernelData;
    GDTEntry UserNull;
    GDTEntry UserCode;
    GDTEntry UserData;
} __attribute__((packed))
__attribute((aligned(0x1000)));

extern GDT DefaultGDT;

extern "C" void LoadGDT(GDTDescriptor* gdtDescriptor);
gdt.cpp

Code: Select all

#include "gdt.h"

GDT DefaultGDT = {
    {0, 0, 0, 0x00, 0x00, 0},
    {0, 0, 0, 0x9a, 0xa0, 0},
    {0, 0, 0, 0x92, 0xa0, 0},
    {0, 0, 0, 0x00, 0x00, 0},
    {0, 0, 0, 0x9a, 0xa0, 0},
    {0, 0, 0, 0x92, 0xa0, 0}
};
gdt.asm (separate assembly file, linked after)

Code: Select all

global LoadGDT
LoadGDT:
    lgdt [edi]
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax

    retf
Loading the table in the main kernel file:

Code: Select all

GDTDescriptor gdtDescriptor;
gdtDescriptor.Size = sizeof(GDT) - 1;
gdtDescriptor.Offset = (uint32_t)&DefaultGDT;

LoadGDT(&gdtDescriptor);
Advice and help would be greatly appreciated!

(I am running in protected mode after booting from GRUB).
Octocontrabass
Member
Member
Posts: 5563
Joined: Mon Mar 25, 2013 7:01 pm

Re: Crash after loading GDT and flushing registers

Post by Octocontrabass »

YDeeps1 wrote:(I am running in protected mode after booting from GRUB).
The descriptors in your GDT are not valid in protected mode, they're only valid in long mode. Are you trying to switch to long mode?

Also, you don't need a second null descriptor, and you probably want user mode to be ring 3 instead of ring 0.
Ethin
Member
Member
Posts: 625
Joined: Sun Jun 23, 2019 5:36 pm
Location: North Dakota, United States

Re: Crash after loading GDT and flushing registers

Post by Ethin »

For the GDT entries, you may not need to attribute it with packed since its 8 bytes long and the compiler (shouldn't) insert any extra padding. But I might be wrong about that.
YDeeps1
Member
Member
Posts: 69
Joined: Tue Aug 31, 2021 7:25 am
Discord: speedy.dev
Contact:

Re: Crash after loading GDT and flushing registers

Post by YDeeps1 »

Octocontrabass wrote:
YDeeps1 wrote:(I am running in protected mode after booting from GRUB).
The descriptors in your GDT are not valid in protected mode, they're only valid in long mode. Are you trying to switch to long mode?

Also, you don't need a second null descriptor, and you probably want user mode to be ring 3 instead of ring 0.
I'm not actually planning to switch to long mode, I'm sticking with 32 bits. I must've gotten confused with the 32 and 64 bit GDT table.
Mind pointing out the invalid descriptors?

For now I'm keeping all my segments kernel level for testing but I'll be sure to change it to ring 3 once the time comes.
Octocontrabass
Member
Member
Posts: 5563
Joined: Mon Mar 25, 2013 7:01 pm

Re: Crash after loading GDT and flushing registers

Post by Octocontrabass »

YDeeps1 wrote:Mind pointing out the invalid descriptors?
The four that aren't null. They describe 64-bit segments, but segments may only be 16-bit or 32-bit in protected mode.
nullplan
Member
Member
Posts: 1790
Joined: Wed Aug 30, 2017 8:24 am

Re: Crash after loading GDT and flushing registers

Post by nullplan »

Also, you are using a far return at the end of the function, but no work has been done to prepare that far return. I think you need something like

Code: Select all

  pop eax
  push 8
  push eax
  retf
at the end there, to actually load your CS with 8.
Carpe diem!
YDeeps1
Member
Member
Posts: 69
Joined: Tue Aug 31, 2021 7:25 am
Discord: speedy.dev
Contact:

Re: Crash after loading GDT and flushing registers

Post by YDeeps1 »

Octocontrabass wrote:
YDeeps1 wrote:Mind pointing out the invalid descriptors?
The four that aren't null. They describe 64-bit segments, but segments may only be 16-bit or 32-bit in protected mode.
Huh? I'm a little confused, I've been using a range of sources as references including the osdev wiki and they all seem to point towards the entries being 32bit (I admit I've been a bit lazy).

So I rewrote some of my code and added a function which automatically deals with spread bits so I can take that frustration out and rewrote my segment descriptors based on the advice here.

Helper function:

Code: Select all

GDTEntry gdt_create_entry(uint32_t base, uint32_t limit, uint8_t access, uint8_t flags) {
    GDTEntry gdt;

    gdt.Limit0 = (uint16_t) (limit & 0xFFFF);
    gdt.Base0 = (uint16_t) (base & 0xFFFF);
    gdt.Base1 = (uint8_t) ((base >> 16) & 0xFF);
    gdt.AccessByte = (uint8_t) access;
    gdt.Limit1_Flags = (uint8_t) ((limit >> 16) & 0x0F);
    gdt.Limit1_Flags |= (uint8_t) (flags & 0xF0);
    gdt.Base2 = (uint8_t) ((base >> 24) & 0xFF);

    return gdt;
}
Segment descriptors:

Code: Select all

GDT DefaultGDT = {
    gdt_create_entry(0, 0, 0, 0), // null segment
    gdt_create_entry(0, 0x8000000, 0x9A, 0x40), // kernel code segment
    gdt_create_entry(0, 0x8000000, 0x92, 0x40), // kernel data segment
    gdt_create_entry(0, 0x8000000, 0xFA, 0x40), // user code segment
    gdt_create_entry(0, 0x8000000, 0xF2, 0x40) // user data segment
};
(the sudden shift from addressing 4gb memory in 4kb pages to addressing 128mb in 1bytes is since I only need 128mb memory at the moment, not the full 4gb).

(i have also set the user segment privilege levels to 3).

From my perspective this descriptor should address all 128mb of memory in 1 byte granules with appropriate execute flags being set and direction bits with the size bit being set to 32bit protected mode (written with the help of https://wiki.osdev.org/Global_Descriptor_Table).

I am also assuming that overlapping segments are fine in this context.
However it is still crashing at that position and I hate not being able to properly debug this myself.

Where did I go wrong?
Octocontrabass
Member
Member
Posts: 5563
Joined: Mon Mar 25, 2013 7:01 pm

Re: Crash after loading GDT and flushing registers

Post by Octocontrabass »

YDeeps1 wrote:Huh? I'm a little confused, I've been using a range of sources as references including the osdev wiki and they all seem to point towards the entries being 32bit (I admit I've been a bit lazy).
You were setting the flags byte to 0xA0, which has the L bit set and the D/B bit clear to indicate a 64-bit segment. For a 32-bit segment, you must clear the L bit and set the D/B bit.
YDeeps1 wrote:Where did I go wrong?
You can't address more than 1 MiB with 1-byte granularity. The limit field has only 20 bits, and the value you're trying to write is being truncated to fit, so you're actually setting the limit to 0, giving you 1-byte segments.
YDeeps1
Member
Member
Posts: 69
Joined: Tue Aug 31, 2021 7:25 am
Discord: speedy.dev
Contact:

Re: Crash after loading GDT and flushing registers

Post by YDeeps1 »

Octocontrabass wrote:
YDeeps1 wrote:Huh? I'm a little confused, I've been using a range of sources as references including the osdev wiki and they all seem to point towards the entries being 32bit (I admit I've been a bit lazy).
You were setting the flags byte to 0xA0, which has the L bit set and the D/B bit clear to indicate a 64-bit segment. For a 32-bit segment, you must clear the L bit and set the D/B bit.
YDeeps1 wrote:Where did I go wrong?
You can't address more than 1 MiB with 1-byte granularity. The limit field has only 20 bits, and the value you're trying to write is being truncated to fit, so you're actually setting the limit to 0, giving you 1-byte segments.
Ah, thank you.

Code: Select all

GDT DefaultGDT = {
    gdt_create_entry(0, 0, 0, 0),
    gdt_create_entry(0, 0x8000, 0x9A, 0xC),
    gdt_create_entry(0, 0x8000, 0x92, 0xC),
    gdt_create_entry(0, 0x8000, 0xFA, 0xC),
    gdt_create_entry(0, 0x8000, 0xF2, 0xC)
};
I have changed the limits to 128mb in 4kb blocks and have hopefully set the two bit flags to set granularity to 4kb blocks and size to 32bit mode (1100). (unless there is something wrong with my bit splitter function?)

It still however crashes; any ideas?
Octocontrabass
Member
Member
Posts: 5563
Joined: Mon Mar 25, 2013 7:01 pm

Re: Crash after loading GDT and flushing registers

Post by Octocontrabass »

YDeeps1 wrote:It still however crashes; any ideas?
How are you calling LoadGDT to make RETF do the right thing?

Is all of your code and data located within your segment limits?

That's all I can think of with the information you've posted so far.
YDeeps1
Member
Member
Posts: 69
Joined: Tue Aug 31, 2021 7:25 am
Discord: speedy.dev
Contact:

Re: Crash after loading GDT and flushing registers

Post by YDeeps1 »

Octocontrabass wrote:
YDeeps1 wrote:It still however crashes; any ideas?
How are you calling LoadGDT to make RETF do the right thing?

Is all of your code and data located within your segment limits?

That's all I can think of with the information you've posted so far.
Right now I am actually just halting right after moving AX into SS to see if it actually loads the table (now that I think about it, doesn't sound like a viable testing approach).

I am pretty certain it is within the segments, I have even double checked the memory locations of some of the stack variables and functions and they're all within the 128mb segment.

For the RETF situation I'm not entirely sure on how to implement it. I did try null's solution but it still crashes, that might not even be the thing that crashes. Maybe my table isn't aligned properly? To reiterate:

I instantiate a GDT, calculate the size and offset of the actual entries then pass the memory location as a parameter to my assembly function (which is in EDI):

Code: Select all

GDTDescriptor gdtDescriptor;
gdtDescriptor.Size = sizeof(GDT) - 1;
gdtDescriptor.Offset = (uint32_t)&DefaultGDT;

LoadGDT(&gdtDescriptor);
(to show my GDT implementation again):

Code: Select all

struct GDTDescriptor {
    uint16_t Size;
    uint32_t Offset;
} __attribute__((packed));

struct GDTEntry {
    uint16_t Limit0;
    uint16_t Base0;
    uint8_t Base1;
    uint8_t AccessByte;
    uint8_t Limit1_Flags;
    uint8_t Base2;
} __attribute__((packed));

struct GDT {
    GDTEntry Null;
    GDTEntry KernelCode;
    GDTEntry KernelData;
    GDTEntry UserCode;
    GDTEntry UserData;
} __attribute__((packed))
__attribute((aligned(0x1000))); // i did have align be sketchy on me on more than one occasion with GCC C++

GDTEntry gdt_create_entry(uint32_t base, uint32_t limit, uint8_t access, uint8_t flags);

extern GDT DefaultGDT; // calls assembly function shown above in another file

extern "C" void LoadGDT(GDTDescriptor* gdtDescriptor);
Octocontrabass
Member
Member
Posts: 5563
Joined: Mon Mar 25, 2013 7:01 pm

Re: Crash after loading GDT and flushing registers

Post by Octocontrabass »

YDeeps1 wrote:then pass the memory location as a parameter to my assembly function (which is in EDI):
Whoops, my bad, I was so focused on the RETF that I didn't even notice that your memory location definitely is not in EDI. There are no 32-bit ABIs where EDI is used to pass parameters. In fact, the default 32-bit ABI passes all parameters on the stack.

You can tell GCC to pass parameters in registers, and the first parameter will be in EAX instead of EDI. It can be configured for individual functions if you don't want it to apply to the whole kernel.
YDeeps1
Member
Member
Posts: 69
Joined: Tue Aug 31, 2021 7:25 am
Discord: speedy.dev
Contact:

Re: Crash after loading GDT and flushing registers

Post by YDeeps1 »

Octocontrabass wrote:
YDeeps1 wrote:then pass the memory location as a parameter to my assembly function (which is in EDI):
Whoops, my bad, I was so focused on the RETF that I didn't even notice that your memory location definitely is not in EDI. There are no 32-bit ABIs where EDI is used to pass parameters. In fact, the default 32-bit ABI passes all parameters on the stack.

You can tell GCC to pass parameters in registers, and the first parameter will be in EAX instead of EDI. It can be configured for individual functions if you don't want it to apply to the whole kernel.
I did actually realise that just before your reply :D I now grab the address directly from the stack (and have made sure this time several times it actually works) but it still crashes which is quite confusing. It's a shame there is no good way to debug this.
Octocontrabass
Member
Member
Posts: 5563
Joined: Mon Mar 25, 2013 7:01 pm

Re: Crash after loading GDT and flushing registers

Post by Octocontrabass »

YDeeps1 wrote:It's a shame there is no good way to debug this.
Sounds like you haven't found this page yet.

Check your VM's triple fault logs before you hook up a whole debugger - sometimes just seeing what state the CPU was in before it crashed is enough to figure out what went wrong for it to get there.
YDeeps1
Member
Member
Posts: 69
Joined: Tue Aug 31, 2021 7:25 am
Discord: speedy.dev
Contact:

Re: Crash after loading GDT and flushing registers

Post by YDeeps1 »

Octocontrabass wrote:
YDeeps1 wrote:It's a shame there is no good way to debug this.
Sounds like you haven't found this page yet.

Check your VM's triple fault logs before you hook up a whole debugger - sometimes just seeing what state the CPU was in before it crashed is enough to figure out what went wrong for it to get there.
I indeed haven't checked the page out.

I was looking through VirtualBox logs and couldn't find (at least to my knowledge) anything which would point me to the issue other than showing me a triple fault has occurred. The last instruction is moving ax into the dx register which matches up with my previous terrible halt debugging technique.

Here are some of the things I thought may be interesting to look at in the logs if you'd like to check them out:
https://hatebin.com/qbnkccaobp
(my intuition tells me some of those things are not good news unless I'm wrong).

Code: Select all

lgdt [edx] ; edx contains the popped GDT table address
mov ax, 0x10
mov ds, ax ; fault occurs here


I have also logged the address of the GDT offset + size table address (decimal 18874372) which matches up with the EDX register, removing some possibilities.

Time for a proper debugger? (updated)
Last edited by YDeeps1 on Tue Sep 14, 2021 3:38 pm, edited 1 time in total.
Post Reply