[SOLVED] General Protection fault in higher half setup code.
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.
[SOLVED] General Protection fault in higher half setup code.
After writing a bunch of memory allocation code and whatnot, I realized that if I wanted to continue I should make my kernel higher-half mapped. So, I went back to the bootloader and the linker and modified them to allow for this. However, I'm getting a really strange #GP with error code 008. I manually inspected the page tables and I swear I didn't see any reserved bit which was set.
possibly important to note is that I'm loading this code with GRUB, on QEMU running a BIOS machine. I debug with lldb. The asm file with my multiboot2 header is not attached, but it also sets up a temporary stack. before calling enterLongMode.
I checked ancient posts here and couldn't find anything helpful. So, any help would be greatly appreciated. :D
GP fault:
long_mode.asm:
linker script:
possibly important to note is that I'm loading this code with GRUB, on QEMU running a BIOS machine. I debug with lldb. The asm file with my multiboot2 header is not attached, but it also sets up a temporary stack. before calling enterLongMode.
I checked ancient posts here and couldn't find anything helpful. So, any help would be greatly appreciated. :D
GP fault:
Code: Select all
check_exception old: 0xffffffff new 0xd
0: v=0d e=0008 i=0 cpl=0 IP=0010:0000000000007d52 pc=0000000000007d52 SP=0018:00000000001269b0 env->regs[R_EAX]=0000000080000011
EAX=80000011 EBX=0027fec0 ECX=c0000080 EDX=00000000
ESI=00000000 EDI=00127000 EBP=00000000 ESP=001269b0
EIP=00007d52 EFL=00000086 [--S--P-] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0018 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
CS =0010 00000000 ffffffff 00cf9a00 DPL=0 CS32 [-R-]
SS =0018 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
DS =0018 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
FS =0018 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
GS =0018 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
LDT=0000 00000000 0000ffff 00008200 DPL=0 LDT
TR =0000 00000000 0000ffff 00008b00 DPL=0 TSS64-busy
GDT= 000000004e05e800 00000000
IDT= 0000000000000000 00000000
CR0=80000011 CR2=わ0000000000000000 CR3=わ0000000000127000 CR4=000000b0
DR0=0000000000000000 DR1=わ0000000000000000 DR2=わ0000000000000000 DR3=わ0000000000000000
DR6=00000000ffff0ff0 DR7=わ0000000000000400
CCS=00200000 CCD=80000011 CCO=LOGICL
EFER=0000000000000500
Code: Select all
section .lowtext
global enterLongmode
; This file serves to transition from 32 bit protected mode to 64 bit long mode
BITS 32
PAGE_TABLE_SIZE equ 4096 ; bytes
; Page table addresses are 4KiB aligned (1024 address bytes, or 0x1000)
; Because of this, the lower 12 bits are set as flags. They're zeroed for the purpose of physical address resolution.
PT_ADDR_MASK equ 0xfffff000
PAGE_PRESENT equ 1 << 0 ; Marks the entry as pointing to a real table
PAGE_RW equ 1 << 1 ; Marks the page as writeable
PAGE_PS equ 1 << 7 ; Marks large pages (in the PDT rather than the PT)
; In protected, each table stores 1024 entries at 32 bits each.
; There are 3 tables. However, we are setting up long.
; In long, there are four tables, 4KiB each, with 512 entires each.
; Each entry in the PT references a 4KiB page.
; Each higher page references an entire lower page table
; PML4T (256 TiB total)
; PDPT (512 GiB total)
; PDT (1 GiB total)
; PT (2 MiB total)
; Load (from the linker) the earliest safe location for the initial pages
extern __pml4t_addr
extern __kernel_start
extern __kernel_data_end
createPageTables:
; We are creating a basic page map.
; First, the PML4T will have a single entry, mapped to the immediately following PDPT, etc.
; Finally, we will fill the PDT with large page entries, identity mapping 1 GiB of lower half memory
; After, we add a second PML4T entry, to higher half map the kernel.
; So 1 PMlT at 0x0000, one PDPT at 0x1000, one PDT at 0x2000, one PDPT at 0x3000, and a last PDT at 0x4000
; =====================
; Blank the page tables
; =====================
mov EDI, __pml4t_addr
xor EAX, EAX
mov ECX, (PAGE_TABLE_SIZE * 5) / 4
; rep stosd writes ECX number of double words, or 4 bytes.
; So we divide ECX by 4
mov DWORD [EDI], 0
rep stosd
; This takes the PDPT address, masks it, marks the proper flags,
; And inserts it into the first entry in the PML4T (base address no offset)
; Then, we repeat it for the first two tables
mov EDI, __pml4t_addr
mov EAX, __pml4t_addr
add EAX, 0x1000 ; Add 4K to get the PDPT address
and EAX, PT_ADDR_MASK ; Mask it out and set the proper bits
or EAX, PAGE_PRESENT | PAGE_RW
mov DWORD [edi], EAX
; Add first entry to the PDPT
mov EDI, __pml4t_addr
add EDI, 0x1000 ; The PDPT address
mov EAX, __pml4t_addr
add EAX, 0x2000 ; The PDT address
and EAX, PT_ADDR_MASK ; Mask it out and set the proper bits
or EAX, PAGE_PRESENT | PAGE_RW
mov DWORD [edi], EAX
; Now, we could do the same for the PDT.
; But, maybe it'd be nicer to instead use 2MiB large pages for now.
; After all, this is temporary. We could change it later.
; ============================
; Identity map the first 1 GiB
; ============================
; Prepare the first entry
lea EAX, 0x0
and EAX, PT_ADDR_MASK ; The mask is 64 bit, but EAX is 32 bit, so we mask the mask
or EAX, PAGE_PRESENT | PAGE_RW | PAGE_PS
mov EDI, __pml4t_addr
add EDI, 0x2000 ; The PDT address
mov ECX, 512 ; Number of entries in the PDT
.loopPDT:
mov DWORD [edi], EAX
; Increment the entry by 2 MiB, then increment the string pointer by 8 (size of entry)
; We only write the lower 4 of each entry, but that's fine,
; because we only map 1 Gib and not more than 4 GiB
add EAX, 0x00200000
lea EDI, [EDI + 8]
loop .loopPDT
; ====================================
; Apply the recursive page table trick
; ====================================
lea EDI, [__pml4t_addr + (511*8)]
mov EAX, __pml4t_addr
and EAX, PT_ADDR_MASK ; Mask it out and set the proper bits
or EAX, PAGE_PRESENT | PAGE_RW
mov [EDI], EAX
ret
mapHigherHalf:
; =======================
; Prepare the higher half
; =======================
; PML4 index for 0xFFFF800000000000 is 256
; So: PML4[256] will point to the PDPT at 0x3000
mov EDI, __pml4t_addr
add EDI, 256 * 8
mov EAX, __pml4t_addr
add EAX, 0x3000 ; PDPT for higher-half
and EAX, PT_ADDR_MASK
or EAX, PAGE_PRESENT | PAGE_RW
mov [EDI], EAX ; PML4[256] = PDPT
; PDPT[0] to PDT at 0x4000
mov EDI, __pml4t_addr
add EDI, 0x3000 ; PDPT for higher half
mov EAX, __pml4t_addr
add EAX, 0x4000 ; PDT for higher-half
and EAX, PT_ADDR_MASK
or EAX, PAGE_PRESENT | PAGE_RW
mov [EDI], EAX
; ===================
; Map the kernel code
; ===================
; We need to calculate how many entries to add.
lea eax, [__kernel_data_end]
lea ebx, [__kernel_start]
sub eax, ebx ; eax = size = end - start
mov ecx, eax
shr ECX, 21 ; Divide by 2 MiB
; Prepare the first entry
lea EAX, [__kernel_start]
and EAX, PT_ADDR_MASK ; The mask is 64 bit, but EAX is 32 bit, so we mask the mask
or EAX, PAGE_PRESENT | PAGE_RW | PAGE_PS
mov EDI, __pml4t_addr
add EDI, 0x4000 ; The PDT address, for higher half
.loopHigherHalf:
mov DWORD [edi], EAX
; Increment the entry by 2 MiB, then increment the string pointer by 8 (size of entry)
; We only write the lower 4 of each entry, but that's fine,
; because we only map 1 Gib and not more than 4 GiB
add EAX, 0x00200000
lea EDI, [EDI + 8]
loop .loopHigherHalf
ret
ENTRIES_PT equ 512
PAGE_SIZE equ 0x1000
; CR4 flags for when we jump into long mode
FLAG_PSE equ 1 << 4 ; Page size extension
FLAG_PAE equ 1 << 5 ; Physical address extension
FLAG_PGE equ 1 << 7 ; Page global enable
; Data used to make the switch into 32 bit compatible long mode thing
EFER_MSR equ 0xC0000080
EFER_LONG_MODE_ENABLE equ 1 << 8
; And for long
CR0_PAGING_ENABLE equ 1 << 31
CR0_PROTECTED_ENABLE equ 1 << 0
; GRUB by default does NOT activate 32 bit paging
; And DOES activate the A20 line
; But keep that in mind if you decide to EFI stub this
enterLongmode:
; Disable 32 bit paging,
; Just in case
mov EAX, CR0
and EAX, ~CR0_PAGING_ENABLE
mov CR0, EAX
; We need the actual page tables set up too
call createPageTables
call mapHigherHalf
; Load the GDT for some reason
lgdt [GDT64.Pointer]
; Set the CR4 flags to configure paging (before we enable it)
mov EAX, CR4
or EAX, FLAG_PGE | FLAG_PAE | FLAG_PSE
mov CR4, EAX
; First 12 bits of CR3 are assumed to be 0/ignored, because of page alignment
; Set CR3 to the start of the PML4T page table
mov EDI, __pml4t_addr
mov CR3, EDI
; MSRs are "machine specific registers"
; This one is no longer machine specific.
; We simply set bit 8 to enable long mode... ish.
mov ECX, EFER_MSR
rdmsr
or EAX, EFER_LONG_MODE_ENABLE
wrmsr
; Not quite. We need to enable paging in CR0 to enter long mode proper, then do a jump
; Right now, we are in compatibility mode (IA-32e)
mov EAX, CR0
or EAX, CR0_PAGING_ENABLE | CR0_PROTECTED_ENABLE
mov CR0, EAX
; The last segment register, cs, can only be set with a long jump
jmp GDT64.Code:trampoline64
; Weird holdover from memory segmentation days
; YES I copied it leave me alone
; Access bits
PRESENT equ 1 << 7
NOT_SYS equ 1 << 4
EXEC equ 1 << 3
DC equ 1 << 2
RW equ 1 << 1
ACCESSED equ 1 << 0
; Flags bits
GRAN_4K equ 1 << 7
SZ_32 equ 1 << 6
LONG_MODE equ 1 << 5
GDT64:
dq 0
; $ is current position. This is an offset
.Code: equ $ - GDT64
.Code.limit_lo: dw 0xffff
.Code.base_lo: dw 0
.Code.base_mid: db 0
.Code.access: db PRESENT | NOT_SYS | EXEC | RW
.Code.flags: db GRAN_4K | LONG_MODE | 0xF ; Flags & Limit (high, bits 16-19)
.Code.base_hi: db 0
.Data: equ $ - GDT64
.Data.limit_lo: dw 0xffff
.Data.base_lo: dw 0
.Data.base_mid: db 0
.Data.access: db PRESENT | NOT_SYS | RW
.Data.flags: db GRAN_4K | SZ_32 | 0xF ; Flags & Limit (high, bits 16-19)
.Data.base_hi: db 0
.Pointer:
dw $ - GDT64 - 1
dq GDT64
section .lowtext
BITS 64
trampoline64:
; One last thing we need to do, is set the segment registers
; Despite the fact that nobody has used segmentation since the 80286
; I guess this tells what segment table (GDT) we are using to various things?
mov AX, GDT64.Data
mov SS, AX ; Stack segment
mov DS, AX ; Data segment
mov ES, AX ; Extra segment
lea rax, [rel start64]
jmp rax
BITS 64
section .text
extern _rust_start
start64:
xor rbp, rbp
jmp _rust_start
Code: Select all
ENTRY(_start)
KERNEL_VMA = 0xFFFFFFFF80000000;
SECTIONS {
. = 1M;
__kernel_start = .;
.boot ALIGN(4K) : AT(0x7C00) SUBALIGN(4) {
KEEP(*(.multiboot_header*))
}
.lowtext : {
KEEP(*(.lowtext*))
}
. += KERNEL_VMA;
.text : AT (ADDR (.text) - KERNEL_VMA) {
KEEP(*(.text*))
KEEP(*(.ltext*))
}
__kernel_code_end = . - KERNEL_VMA;
.rodata : AT (ADDR (.rodata) - KERNEL_VMA) {
KEEP(*(.rodata*))
KEEP(*(.lrodata*))
}
.bss ALIGN(16) : AT (ADDR (.bss) - KERNEL_VMA) {
__temp_stack_bottom = LOADADDR(.bss) + (. - ADDR(.bss));
. += 16K;
__temp_stack_top = LOADADDR(.bss) + (. - ADDR(.bss));
. = ALIGN(4K);
__pml4t_addr = LOADADDR(.bss) + (. - ADDR(.bss));
. += 20K;
}
. = ALIGN(0x200000) + __kernel_start;
__kernel_data_end = . - KERNEL_VMA;
}
Last edited by closos on Fri Sep 05, 2025 10:34 am, edited 1 time in total.
- MichaelPetch
- Member
Member - Posts: 848
- Joined: Fri Aug 26, 2016 1:41 pm
- Libera.chat IRC: mpetch
Re: General Protection fault in higher half setup code.
Post by MichaelPetch »
I did see this with the GPF:That look like a corrupt GDTR was passed to LGDT. The address looks wrong as does the size (0). I'd recommends trying to use the BOCHS debugger to step through your code and see why this occurs.
Code: Select all
GDT= 000000004e05e800 00000000
Re: General Protection fault in higher half setup code.
Yeah, that does look pretty weird:
Ghidra gives this
0x001001d7 looks normal to me as an address. Not sure what's going on
Ghidra gives this
Code: Select all
GDT64.Pointer
001001ef 17 ?? 17h
001001f0 00 ?? 00h
001001f1 d7 ?? D7h ? -> 001001d7
001001f2 01 ?? 01h
001001f3 10 ?? 10h
001001f4 00 ?? 00h
001001f5 00 ?? 00h
001001f6 00 ?? 00h
001001f7 00 ?? 00h
001001f8 00 ?? 00h
Re: General Protection fault in higher half setup code.
Code: Select all
* thread #1, stop reason = instruction step into
frame #0: 0x0000000000007dc6
-> 0x7dc6: movq %rax, %cr0
0x7dc9: lgdtq 0x1001ef(%rip)
(lldb) memory read 0x1001ef
0x001001ef: 00 83 c4 10 eb 1f 01 f6 eb e2 89 f2 8b 03 e8 b2 ................
0x001001ff: 50 00 00 85 c0 75 07 a1 e0 7d 01 00 eb 07 89 03 P....u...}......
(lldb) memory read 0x1001ef -f x -c 4
0x001001ef: 0x10c48300 0xf6011feb 0xf289e2eb 0xb2e8038b
(lldb)
- MichaelPetch
- Member
Member - Posts: 848
- Joined: Fri Aug 26, 2016 1:41 pm
- Libera.chat IRC: mpetch
Re: General Protection fault in higher half setup code.
Post by MichaelPetch »
I don't think it is helping that you seem to be requesting code be loaded with a LMA of 0x7C00 and referencing things with a VMA of 0x001xxxxx. I'd be curious about the design decision to use 0x7C00 this way in the linker script: I'm assuming your _start entry point (which you don't show) is also in .lowtext as well.
Code: Select all
. = 1M;
__kernel_start = .;
.boot ALIGN(4K) : AT(0x7C00) SUBALIGN(4) {
KEEP(*(.multiboot_header*))
}
.lowtext : {
KEEP(*(.lowtext*))
}
Re: General Protection fault in higher half setup code.
Oh, that was left in accidentally. I was playing around with forcing the linker to put the multiboot header at the start, because that was an issue I had been having a bit ago. Removing it, it boots properly still, and the error changes to something which seems to just be a bog-standard page fault. Thank you!
- MichaelPetch
- Member
Member - Posts: 848
- Joined: Fri Aug 26, 2016 1:41 pm
- Libera.chat IRC: mpetch
Re: [SOLVED] General Protection fault in higher half setup code.
Post by MichaelPetch »
I also just noticed that your KERNEL VMA in the boot code (paging layout) is using 0xFFFF800000000000 but the linker script is using 0xFFFFFFFF80000000. You will have to modify things so you are using the same KERNEL VMA. I'd recommend going with 0xFFFFFFFF80000000 as you can then use the `-mcmodel=kernel` GCC option.
- MichaelPetch
- Member
Member - Posts: 848
- Joined: Fri Aug 26, 2016 1:41 pm
- Libera.chat IRC: mpetch
Re: [SOLVED] General Protection fault in higher half setup code.
Post by MichaelPetch »
As well when you do the `mapHigherHalf` you will need to round `__kernel_start` down to the nearest 2MiB boundary because 2MiB pages will have to be aligned on 2MiB boundaries. You can do this by ANDing the address of`__kernel_start` with `-0x200000` (note the negative). When you compute the total size of the kernel to map you will want to round the result up to the nearest 2MiB to ensure all the code and data is captured. You can do this by adding (0x200000-1=0x1FFFFF) to the value and then shift that right 21 bits (2MiB).