GPF, probably caused by incorrect GDT

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.
Hamster1800
Posts: 14
Joined: Wed Apr 16, 2008 5:12 pm

GPF, probably caused by incorrect GDT

Post by Hamster1800 »

Hi all,

A while back, a friend and I tried to write a kernel in C following a tutorial. After enabling interrupts, it would receive a never ending stream of GPFs, which were correctly handled by the IDT. However, adding random bits of C code here and there made the kernel either work or break, depending on how gcc felt like compiling it. Thus, we gave up on the project.

Now, I am attempting to rewrite the kernel, but I am writing the simple portions in assembly, since I know much more about how assemblers assemble assembly than how compilers compile C. However, my new kernel errors out in the same place in the same way. Something is going wrong in my code, and I have no idea what it is.

I have run the kernel on my laptop directly, in qemu, and in bochs. Each of them causes it to receive a never-ending stream of GPFs. However, the associated error code changes slightly. On my laptop, I receive an error code of 528 whereas both qemu and bochs get error code 512. I have not been able to find any reference on what these error codes mean, so I haven't been able to debug it that way. However, when running in bochs, each GPF is associated with the following error message:

00007573171e[CPU0 ] fetch_raw_descriptor: GDT: index (207)40 > limit (17)

where the initial hex number increments by 0x30 each time.

I have figured out that this error message means that something is attempting to access 0x40 in the GDT, but for the life of me I can't figure out what that something is.

Here is the code which should set up the GDT. I have compared it to many working kernels, and they are essentially identical, the only differences being AT&T v. Intel syntax and far jumping before/after loading the other segment registers.

Code: Select all

global init_gdt

align 32

init_gdt:
        lgdt [gdtr]

        mov ax,0x10
        mov ds,ax
        mov es,ax
        mov gs,ax
        mov fs,ax
        mov ss,ax

        jmp 0x08:gdt_return

gdt_return:
        ret

section .data
the_gdt:
null_desc:
        db 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
flat_code:
        db 0xff,0xff,0x00,0x00,0x00,0x9a,0xcf,0x00
flat_data:
        db 0xff,0xff,0x00,0x00,0x00,0x92,0xcf,0x00
gdt_end:

gdtr:
        dw gdt_end-the_gdt-1
        dd the_gdt
And I am calling it with simply

Code: Select all

call init_gdt
An interesting bit of odd behavior which I discovered (which may have the same root as the bug) is that if I place an infinite loop after this line:

Code: Select all

mov ax,0x10
The GDT looks like it should (looking at it via qemu).

However, if i place an infinite loop after the next line:

Code: Select all

mov ax,0x10
mov ds,ax
One byte of the GDT changes, and the segment does not get properly updated as a result. This byte is the 0x92, which gets changed to 0x93.

I have searched these forums and other locations for similar behavior, but with no success. Perhaps one of you has encountered a similar error and could help shed some light on the situation?
User avatar
Zenith
Member
Member
Posts: 224
Joined: Tue Apr 10, 2007 4:42 pm

Post by Zenith »

Why are you RETURNING from gdt_return? WHY?

If you actually understand what you're doing, you should know the 'ret' opcode can only return to a 'call' statement, as it pops some values (CS, EIP) previously pushed by the call. But since you're (far) jumping, none of these values are pushed and so the 'ret' statement reads invalid values from the stack.

That return may change the CS value to point to some uninitialized GDT entry, with invalid values, causing a GPF. If you're lucky enough, it might actually return to your jmp and execute the return which unwittingly follows it...

Try replacing the 'ret' statement with an infinite loop, and see what happens. :wink:
One byte of the GDT changes, and the segment does not get properly updated as a result. This byte is the 0x92, which gets changed to 0x93.
That's because the processor sets the accessed bit for the data segment type as soon as you use it. Just one of those little things... Your GDT is fine.
"Sufficiently advanced stupidity is indistinguishable from malice."
Hamster1800
Posts: 14
Joined: Wed Apr 16, 2008 5:12 pm

Post by Hamster1800 »

I am not quite certain what your objection to the ret statement is. I call init_gdt, which far jumps to gdt_return, which executes the ret statement. If I'm understanding correctly, which I understand that I might not be, it should pop the values from the call to init_gdt, and return to where I want.

Testing what you suggested, if I replace the ret with an infinite loop, I indeed do not get a GPF (as far as I can tell). However, I want to return to the boot code (to set up the IDT, etc). Is there a nice way to do this?
User avatar
neon
Member
Member
Posts: 1567
Joined: Sun Feb 18, 2007 7:28 pm
Contact:

Post by neon »

karekare0 wrote:But since you're (far) jumping, none of these values are pushed and so the 'ret' statement reads invalid values from the stack.
I just want to clarify here:

ret pops the return cs:eip address from the stack. The return cs:eip was pushed when you called the routine (Using call instruction). Because you called the routine from real mode, your cs:eip contains your real mode seg:offset return address.

This is where the problem is at. PMode uses desc:linear addressing mode, where desc is the descriptor offset in your GDT, and linear is a 32 bit linear address. So, the processor will use the popped CS value as a descriptor offset. If it points to an invalid descriptor, or--in your case--is greater then the size of the GDT stored in GDTR.length, the processor generates a #gpf.
If you're lucky enough, it might actually return to your jmp and execute the return which unwittingly follows it...
That can never happen. The value of eip in real mode which was pushed on the stack contains an offset address. When returning from pmode, it will be treated as a 32 bit linear address do to pmode addressing mode.
OS Development Series | Wiki | os | ncc
char c[2]={"\x90\xC3"};int main(){void(*f)()=(void(__cdecl*)(void))(void*)&c;f();}
Hamster1800
Posts: 14
Joined: Wed Apr 16, 2008 5:12 pm

Post by Hamster1800 »

I'm booting via GRUB, so (if my understanding is correct) I should be in protected mode when I gain control. Second, I examined the stack during the infinite loop which I placed instead of the ret, and the value which would be popped into cs appears to be the same as what I want (0x08), although this is more likely by pure chance than defined behavior. Even so, if I place the infinite loop directly after where it should return to, it GPFs.

Thus, I am certain that you are correct in that the ret is wrong, but I haven't figured out why based on the posts above. Could you elaborate?

Oh yeah, thanks for clarifying the thing about the 0x92->0x93 deal. That gives me a bit of peace of mind.
User avatar
neon
Member
Member
Posts: 1567
Joined: Sun Feb 18, 2007 7:28 pm
Contact:

Post by neon »

Hamster1800 wrote:I'm booting via GRUB, so (if my understanding is correct) I should be in protected mode when I gain control.
Oh.. o_o Never mind my previous post then--you are correct.
Hamster1800 wrote:Second, I examined the stack during the infinite loop which I placed instead of the ret, and the value which would be popped into cs appears to be the same as what I want (0x08)
If CS already contains the descriptor offset that you want (0x8), then take out your far jump--its redundant, and is not necessary when setting up a new GDT in your case. (You only need to far jump when resetting CS to a new descriptor offset)

Also, resetting SS to a new value can cause big stack problems when returning (assuming it was different then 0x10):

Code: Select all

init_gdt:
        lgdt [gdtr]

; I recommend removing this section, tbh. Thats up to you, though.
; The reason is to provide a good functional interface. In fact, init_gdt
; should be renamed to install_gdt, load_gdtr, et al...Just something that
; describes what it does--and no more

        mov ax,0x10
        mov ds,ax
        mov es,ax
        mov gs,ax
        mov fs,ax
        ret
Last edited by neon on Wed Apr 16, 2008 7:51 pm, edited 2 times in total.
OS Development Series | Wiki | os | ncc
char c[2]={"\x90\xC3"};int main(){void(*f)()=(void(__cdecl*)(void))(void*)&c;f();}
User avatar
Zenith
Member
Member
Posts: 224
Joined: Tue Apr 10, 2007 4:42 pm

Post by Zenith »

You're using GRUB? Now, that explains a lot!
Hamster1800 wrote:If I'm understanding correctly, which I understand that I might not be, it should pop the values from the call to init_gdt, and return to where I want.
Note that the jmp instruction is not a 'call'. It does not push the necessary values onto the (newly initialized, since you just moved a fresh segment selector into ss) stack.

My theory was actually (well, partially) correct :shock:! Since GRUB does set protected mode, it also sets up a GDT which does set CS to 0x08 - not chance, just GRUB. :wink:

If you want to continue executing your code, just 'jmp' to your IDT setup code, initialization function, etc. from gdt_return! Just get rid of the ret and continue your code as normal.

Hope this helps!

ADDED:
Whoops, neon beat me to the reply, but I'd like to clarify some things:

GRUB leaves your kernel in an undefined state. While CS=0x8 now, it won't necessarily be in a different boot/version/loader. The far jump is the only way you can start executing known-good code with a known-good GDT.
Also, resetting SS to a new value can cause big stack problems when returning (assuming it was different then 0x10):
No! NOT resetting SS will cause big stack problems! The multiboot spec does guarantee, however that the base of SS will always be 0x0, so the stack will remain unaffected as long as the base of your segment selector remains at 0 as well.
"Sufficiently advanced stupidity is indistinguishable from malice."
User avatar
neon
Member
Member
Posts: 1567
Joined: Sun Feb 18, 2007 7:28 pm
Contact:

Post by neon »

karekare0 wrote:Note that the jmp instruction is not a 'call'. It does not push the necessary values onto the (newly initialized, since you just moved a fresh segment selector into ss) stack.
His init_gdt is the routine that is CALLed upon to enter pmode. That is where the call is at. (And also why he RETurns from it.)
karekare0 wrote:GRUB leaves your kernel in an undefined state. While CS=0x8 now, it won't necessarily be in a different boot/version/loader. The far jump is the only way you can start executing known-good code with a known-good GDT.
I personally do not use GRUB, so didnt know that. Thanks for clearing that up.

I would agree with you here, then. :)
karekare0 wrote:No! NOT resetting SS will cause big stack problems! The multiboot spec does guarantee, however that the base of SS will always be 0x0, so the stack will remain unaffected as long as the base of your segment selector remains at 0 as well.
You know what? You are absolutely correct here. (Tired. I was thinking of esp for some reason)
Last edited by neon on Wed Apr 16, 2008 8:09 pm, edited 3 times in total.
OS Development Series | Wiki | os | ncc
char c[2]={"\x90\xC3"};int main(){void(*f)()=(void(__cdecl*)(void))(void*)&c;f();}
Hamster1800
Posts: 14
Joined: Wed Apr 16, 2008 5:12 pm

Post by Hamster1800 »

So I somewhat understand what you are saying, but the behavior after making various changes confuses me.

As it is, if I take out the infinite loop, the ret works perfectly, the code continues executing as it should (in particular the IDT gets set up after the GDT), but it GPFs.

If I put the infinite loop before the ret, I don't get a GPF. The IDT is not set up yet, so I enabled interrupts to check for a triple fault and I didn't get one, so nothing bad seems to be happening.

I tried commenting out updating ss and updating it after the ret, but it still GPFs.

So it rets to the right location, but it GPFs, which leaves me in a position where I have a clear way to make it work (continue initialization without ret), but I would prefer a way to ret without causing a GPF.
User avatar
Zenith
Member
Member
Posts: 224
Joined: Tue Apr 10, 2007 4:42 pm

Post by Zenith »

neon wrote:His init_gdt is the routine that is CALLed upon to enter pmode. That is where the call is at. (And also why he RETurns from it.)
Oh, I see. I thought we were talking about the far jmp from init_gdt to gdt_return. Yeah, I was hastily assuming that he was directly executing the code, not calling it from some other procedure. Big mistake on my part.

Best solution - don't 'call' init_gdt, 'jmp' to it, or integrate it to the calling function directly. Then you avoid having to return to anything, and having to deal with an invalid CS value - which you will have to deal with if your new CS differs from the GRUB sets up. The far jmp is ESSENTIAL.

The physical value in 'SS' does not matter - only that the bases, limits, etc. must remain the same.

So, basically:

Original code, I'm assuming:

Code: Select all

call init_gdt
should become either

Code: Select all

jmp init_gdt
continue_exec:
with which you'll only have to far jmp to 0x8:continue_exec to continue doing loading stuff, or replace call/jmp with the actual code:

Code: Select all

lgdt [gdtr]
(other code)
jmp 0x8:gdt_return

gdt_return:
(continue here)
Hope this helps![/u]
"Sufficiently advanced stupidity is indistinguishable from malice."
Hamster1800
Posts: 14
Joined: Wed Apr 16, 2008 5:12 pm

Post by Hamster1800 »

I am somewhat mystified. I tried your suggestion, replacing

Code: Select all

call init_gdt
with

Code: Select all

        lgdt [gdtr]
        mov ax,0x10
        mov ds,ax
        mov es,ax
        mov gs,ax
        mov fs,ax
        mov ss,ax
        jmp 0x08:continue_exec

continue_exec:
and it gives me the same GPF.

I also tried jmping back and forth and it gave the same GPF. Plus, it now gives me a GPF even if I just stay in init_gdt, so I must have done something to break it (or it never worked and I didn't realize it), but at least it's consistent with itself now.
User avatar
Combuster
Member
Member
Posts: 9301
Joined: Wed Oct 18, 2006 3:45 am
Libera.chat IRC: [com]buster
Location: On the balcony, where I can actually keep 1½m distance
Contact:

Post by Combuster »

Lets start with some proper debugging: run everything in bochs, let the GPF happen and post the log, which tells you in most cases what is causing the problem.
"Certainly avoid yourself. He is a newbie and might not realize it. You'll hate his code deeply a few years down the road." - Sortie
[ My OS ] [ VDisk/SFS ]
User avatar
bewing
Member
Member
Posts: 1401
Joined: Wed Feb 07, 2007 1:45 pm
Location: Eugene, OR, US

Post by bewing »

Isn't there an additional problem -- that the GDTR structure is misaligned? The dd is not on a dword boundary.

Try this and see if anything changes.

Code: Select all

gdt_end: 

	dw	0	; force proper alignment
gdtr: 
	dw	gdt_end-the_gdt-1 
	dd	the_gdt 
Hamster1800
Posts: 14
Joined: Wed Apr 16, 2008 5:12 pm

Post by Hamster1800 »

I tried adding an extra word, and it gave the same thing.

I posted the log here: http://pastebin.com/m3aa8f4b8
User avatar
Zenith
Member
Member
Posts: 224
Joined: Tue Apr 10, 2007 4:42 pm

Post by Zenith »

Basically, the GPF is saying that you're trying to run code / read data with a selector pointing to index 40, and that this index is above the GDT limit you set in the GDTR.

@Hamster1800: Try showing us the code that's executing before, in and after init_gdt.

@bewing: The GDT struct does not have to be aligned, since LGDT loads from any 32/64-bit memory address.
"Sufficiently advanced stupidity is indistinguishable from malice."
Post Reply