Triple Fault when entering Long Mode [FIXED]

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.
Post Reply
proto639
Posts: 10
Joined: Sun Oct 15, 2023 12:07 am

Triple Fault when entering Long Mode [FIXED]

Post by proto639 »

Good morning, I'm working on a small hobby OS project and so far I'm stuck on getting long mode working,
it seems to work but if I do a segmented/far jump it triple faults and gets into a reboot loop, I've been losing my mind trying to figure out this error
I have attached my code down below.
I am using QEMU to debug, and I can't pinpoint the issue even with
-d cpu
or
-d int
args, any help?
Attachments

[The extension s has been deactivated and can no longer be displayed.]

Last edited by proto639 on Sun Oct 15, 2023 7:24 pm, edited 1 time in total.
nullplan
Member
Member
Posts: 1789
Joined: Wed Aug 30, 2017 8:24 am

Re: Triple Fault when entering Long Mode

Post by nullplan »

There are quite a few problems with your code. First, you are mapping the first 1GB of address space with 2MB huge pages. However, you are not supposed to use huge pages that span multiple memory types, and it is likely that the memory types are going to be mixed at the end of the first 1MB.

Your GDT has all zero segment limits. That might not matter in 64-bit mode, but it will kill you while you are in 32-bit mode after loading the GDT and the segments. Suddenly no data access will work anymore. And finally, that transfer to long mode should probably be done with a far jmp instead of a push.

Personally, I was never a fan of these mixed-environment kernels some tutorials peddle. I have a separate binary that sets up paging, mapping the kernel according to its ELF headers and setting up a few other things, before jumping to the ELF entry point. This binary reads the physical memory info from its boot environment, dynamically allocates whatever space is needed, and adds all of the allocated memory to the list of "reserved" addresses it gives the main kernel. Doing this means I have no issue using 4kB pages.
Carpe diem!
proto639
Posts: 10
Joined: Sun Oct 15, 2023 12:07 am

Re: Triple Fault when entering Long Mode

Post by proto639 »

nullplan wrote:There are quite a few problems with your code. First, you are mapping the first 1GB of address space with 2MB huge pages. However, you are not supposed to use huge pages that span multiple memory types, and it is likely that the memory types are going to be mixed at the end of the first 1MB.

Your GDT has all zero segment limits. That might not matter in 64-bit mode, but it will kill you while you are in 32-bit mode after loading the GDT and the segments. Suddenly no data access will work anymore. And finally, that transfer to long mode should probably be done with a far jmp instead of a push.

Personally, I was never a fan of these mixed-environment kernels some tutorials peddle. I have a separate binary that sets up paging, mapping the kernel according to its ELF headers and setting up a few other things, before jumping to the ELF entry point. This binary reads the physical memory info from its boot environment, dynamically allocates whatever space is needed, and adds all of the allocated memory to the list of "reserved" addresses it gives the main kernel. Doing this means I have no issue using 4kB pages.
My bad about the push, I was testing out some other methods I saw.
How big are pages usually? The tutorial I saw used 2MB pages, also where do you see the zero segment limits? I can't really read the GDT all that well, apologies.
nullplan
Member
Member
Posts: 1789
Joined: Wed Aug 30, 2017 8:24 am

Re: Triple Fault when entering Long Mode

Post by nullplan »

proto639 wrote:My bad about the push, I was testing out some other methods I saw.
How big are pages usually? The tutorial I saw used 2MB pages, also where do you see the zero segment limits? I can't really read the GDT all that well, apologies.
Pages are normally 4kB. You can use pages larger than that (namely 2MB and 1GB pages), but those are huge pages, and there are limitations to their use. They cannot span areas with multiple memory types, for one thing. Most OSes implement 4kB paging, with optional use of huge pages if the situation permits, and having a consistent memory type is one of those requirements. In order to test for that, you'd need to worry about MTRRs, so maybe you should skip that for now and only do the 4kB paging.

The segment limit is part of the GDT entry, namely the low 16 bits, bits 48-51, and the G bit (bit 55). They set how big the segment is (more exactly: They set the highest offset in the segment.). I also at first didn't understand your segments. I am used to looking at them in hex now, because the fields do line up well with 4-bit boundaries. You know, it is basically

Code: Select all

code: dq 0xaaflptaaaaaallll
Where a is the base address, l is the limit, f is the flags nibble (the G, D, L, and AVL bits), p is the permissions nibble (the P, DPL, and S fields), and t is the type nibble. The GDT you have in your code amounts to:

Code: Select all

section .rodata
gdt64:
    dq 0
.code: equ $ - gdt64
    dq 0x00209a0000000000
.data: equ $ - gdt64
    dq 0x0000920000000000
.pointer:
    dw .pointer - gdt64 - 1
    dq gdt64
And if you load the second one in 32-bit mode (which you do), then you get a segmentation fault for accessing any data item beyond offset 0. But it is easily fixed if you just change it to

Code: Select all

.code: dq 0x00af9a000000ffff
.data: dq 0x00cf92000000ffff
It takes the exact same amount of space, but now the limit is 4GB. Don't worry, once you get to 64-bit mode, the limit will be ignored.
Carpe diem!
proto639
Posts: 10
Joined: Sun Oct 15, 2023 12:07 am

Re: Triple Fault when entering Long Mode

Post by proto639 »

nullplan wrote:
proto639 wrote:My bad about the push, I was testing out some other methods I saw.
How big are pages usually? The tutorial I saw used 2MB pages, also where do you see the zero segment limits? I can't really read the GDT all that well, apologies.
Pages are normally 4kB. You can use pages larger than that (namely 2MB and 1GB pages), but those are huge pages, and there are limitations to their use. They cannot span areas with multiple memory types, for one thing. Most OSes implement 4kB paging, with optional use of huge pages if the situation permits, and having a consistent memory type is one of those requirements. In order to test for that, you'd need to worry about MTRRs, so maybe you should skip that for now and only do the 4kB paging.

The segment limit is part of the GDT entry, namely the low 16 bits, bits 48-51, and the G bit (bit 55). They set how big the segment is (more exactly: They set the highest offset in the segment.). I also at first didn't understand your segments. I am used to looking at them in hex now, because the fields do line up well with 4-bit boundaries. You know, it is basically

Code: Select all

code: dq 0xaaflptaaaaaallll
Where a is the base address, l is the limit, f is the flags nibble (the G, D, L, and AVL bits), p is the permissions nibble (the P, DPL, and S fields), and t is the type nibble. The GDT you have in your code amounts to:

Code: Select all

section .rodata
gdt64:
    dq 0
.code: equ $ - gdt64
    dq 0x00209a0000000000
.data: equ $ - gdt64
    dq 0x0000920000000000
.pointer:
    dw .pointer - gdt64 - 1
    dq gdt64
And if you load the second one in 32-bit mode (which you do), then you get a segmentation fault for accessing any data item beyond offset 0. But it is easily fixed if you just change it to

Code: Select all

.code: dq 0x00af9a000000ffff
.data: dq 0x00cf92000000ffff
It takes the exact same amount of space, but now the limit is 4GB. Don't worry, once you get to 64-bit mode, the limit will be ignored.
Thank you so much! It does enter into 64-bit mode, however I'm now facing an entirely new issue.
Whenever I do my long jump into my long mode label with this:

Code: Select all

jmp gdt64.code:long_mode_start
, it seems the area it jumps to is bogus and is filled with zeros, QEMU with -d in_asm shows this whenever it jumps:

Code: Select all

0x010006f8:  00 00                    addb     %al, (%rax)

and it goes on for several seconds, until it seemingly triple-faults again (probably unrelated to the GDT) and shuts down
this is what the jump instruction shows according to QEMU:

Code: Select all

0x0010130b:  ea 12 03 00 01 08 00     ljmpl    $0x8:$0x1000312

----------------
IN: 
0x01000312:  00 00                    addb     %al, (%rax)
Octocontrabass
Member
Member
Posts: 5560
Joined: Mon Mar 25, 2013 7:01 pm

Re: Triple Fault when entering Long Mode

Post by Octocontrabass »

proto639 wrote:

Code: Select all

0x0010130b:
0x01000312:
Those two addresses are awfully far apart. How are you linking your binary?
proto639
Posts: 10
Joined: Sun Oct 15, 2023 12:07 am

Re: Triple Fault when entering Long Mode

Post by proto639 »

Octocontrabass wrote:
proto639 wrote:

Code: Select all

0x0010130b:
0x01000312:
Those two addresses are awfully far apart. How are you linking your binary?
You're right, I just noticed how far they really are, I think I have my .text around 0x010000? Here's my linker script if you need it:

Code: Select all

ENTRY(start)

SECTIONS {
    . = 1M;
    _kernel_physical_start = .;

    .boot :
    {
        *(.multiboot_header)
    }

    . = ALIGN(0x1000);
    _boot_end = .;

    . += 0xFFFFFFFF80000000;
    _kernel_virtual_start = .;
    .text : AT(_boot_end)
    {
        *(.multiboot)
        *(.text)
    }
} 
If you want to see everything, here's the repository:
https://github.com/ocso639/divisix/
proto639
Posts: 10
Joined: Sun Oct 15, 2023 12:07 am

Re: Triple Fault when entering Long Mode

Post by proto639 »

oh god how did i miss that
I got rid of that, but now my linker complains about this:

Code: Select all

long-mode.o: in function `start':
long-mode.s:(.text+0x1): relocation truncated to fit: R_X86_64_32 against `.bss'
long-mode.s:(.text+0x9): relocation truncated to fit: R_X86_64_32 against `.bss'
long-mode.s:(.text+0xe): relocation truncated to fit: R_X86_64_32 against `.bss'
long-mode.s:(.text+0x16): relocation truncated to fit: R_X86_64_32 against `.bss'
long-mode.o: in function `start.map_p2_table':
long-mode.s:(.text+0x2e): relocation truncated to fit: R_X86_64_32 against `.bss'
long-mode.s:(.text+0x3c): relocation truncated to fit: R_X86_64_32 against `.bss'
long-mode.s:(.text+0x7c): relocation truncated to fit: R_X86_64_32 against `.text'
make: *** [makefile:15: all] Error 1
I'm having a hard time trying to understand what it means by this
proto639
Posts: 10
Joined: Sun Oct 15, 2023 12:07 am

Re: Triple Fault when entering Long Mode

Post by proto639 »

Nevermind, I changed my linker script and I managed to fix it up, it's working perfectly now!
Thank you all for your help
Octocontrabass
Member
Member
Posts: 5560
Joined: Mon Mar 25, 2013 7:01 pm

Re: Triple Fault when entering Long Mode

Post by Octocontrabass »

proto639 wrote:

Code: Select all

relocation truncated to fit: R_X86_64_32
Your code is referencing symbols in the .text and .bss sections using instructions that only support unsigned 32-bit addresses. Your linker script assigns virtual addresses for .text and .bss somewhere above 0xFFFFFFFF80000000, so the addresses can't be represented as unsigned 32-bit values.

Since it looks like all of those references occur in the 32-bit startup code that runs before you've jumped to the higher half, you could solve the problem by moving those symbols to a section that isn't in the higher half. Or, if you want to be clever about it, you could add an offset to each symbol so the linker can insert the higher-half addresses but the added offset results in the correct lower-half address. (The linker might still complain that the symbol+offset doesn't fit in 32 bits.)

Or, you can avoid the whole problem by not trying to include the 32-bit setup code in your 64-bit kernel binary.
proto639 wrote:Nevermind, I changed my linker script and I managed to fix it up, it's working perfectly now!
...You sure about that? Your linker script doesn't specify where .bss, .data, or .rodata will go, so things might not be working as well as you think. (And don't forget wildcards! If you use ".rodata" instead of ".rodata*" you'll miss sections with names like ".rodata.str.1".)
proto639
Posts: 10
Joined: Sun Oct 15, 2023 12:07 am

Re: Triple Fault when entering Long Mode

Post by proto639 »

Octocontrabass wrote:
proto639 wrote:

Code: Select all

relocation truncated to fit: R_X86_64_32
Your code is referencing symbols in the .text and .bss sections using instructions that only support unsigned 32-bit addresses. Your linker script assigns virtual addresses for .text and .bss somewhere above 0xFFFFFFFF80000000, so the addresses can't be represented as unsigned 32-bit values.

Since it looks like all of those references occur in the 32-bit startup code that runs before you've jumped to the higher half, you could solve the problem by moving those symbols to a section that isn't in the higher half. Or, if you want to be clever about it, you could add an offset to each symbol so the linker can insert the higher-half addresses but the added offset results in the correct lower-half address. (The linker might still complain that the symbol+offset doesn't fit in 32 bits.)

Or, you can avoid the whole problem by not trying to include the 32-bit setup code in your 64-bit kernel binary.
proto639 wrote:Nevermind, I changed my linker script and I managed to fix it up, it's working perfectly now!
...You sure about that? Your linker script doesn't specify where .bss, .data, or .rodata will go, so things might not be working as well as you think. (And don't forget wildcards! If you use ".rodata" instead of ".rodata*" you'll miss sections with names like ".rodata.str.1".)
It does boot, but now that you mention it there might be a hidden problem somewhere, waiting for the right moment to strike
I need to include 32 bit setup code in my binary as GRUB switches to protected mode (I don't know what it does under EFI, so I may be wrong if it's long mode in EFI)
Octocontrabass
Member
Member
Posts: 5560
Joined: Mon Mar 25, 2013 7:01 pm

Re: Triple Fault when entering Long Mode

Post by Octocontrabass »

proto639 wrote:I need to include 32 bit setup code in my binary as GRUB switches to protected mode
You can have two separate binaries. GRUB can load as many files as you like in the form of Multiboot modules; just tell it your kernel is one of the modules it should load and then your kernel can be separate from the 32-bit setup code.
proto639 wrote:(I don't know what it does under EFI, so I may be wrong if it's long mode in EFI)
It does the same thing. (And EFI can be 32-bit too.)

Now that I think of it, you might be interested in a more modern bootloader such as Limine.
proto639
Posts: 10
Joined: Sun Oct 15, 2023 12:07 am

Re: Triple Fault when entering Long Mode

Post by proto639 »

Octocontrabass wrote:
proto639 wrote:I need to include 32 bit setup code in my binary as GRUB switches to protected mode
You can have two separate binaries. GRUB can load as many files as you like in the form of Multiboot modules; just tell it your kernel is one of the modules it should load and then your kernel can be separate from the 32-bit setup code.
proto639 wrote:(I don't know what it does under EFI, so I may be wrong if it's long mode in EFI)
It does the same thing. (And EFI can be 32-bit too.)
Maybe, that does seem more efficient as I don't need to recompile the kernel, I just need to recompile the long mode loader and keep the kernel as-is
Octocontrabass wrote:Now that I think of it, you might be interested in a more modern bootloader such as Limine.
I could try that, but I'm most familiar with GRUB

I just looked further into Limine, and I see there's SMP requests (or whatever they're called), I think I might try that..
Post Reply