[SOLVED] Triple Fault on Setting SS Register

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
Tyyrus
Posts: 9
Joined: Tue May 26, 2020 8:44 pm

[SOLVED] Triple Fault on Setting SS Register

Post by Tyyrus »

Hello all,

I've been getting a triple fault when trying to set the SS register after loading my GDT. I was getting a bunch of very strange GP faults when working on interrupts, and realized I hadn't set the segment registers after creating and loading my GDT. When I tried to set them, it works for all of the registers except the SS register. The OS is 64-bit, already in long mode, and loaded via UEFI. I'm running it on a VirtualBox machine. I don't want to flood the post with too much code, but here's my GDT loading function

Code: Select all

GDTCodeOrDataEntry* initializeGDT(PhysicalMemoryLayout* physMemLayout) {
	GDTCodeOrDataEntry* gdt = (GDTCodeOrDataEntry*) AllocatePage(physMemLayout);

	gdt[0] = (GDTCodeOrDataEntry) { 0, 0, 0, 0, 0, 0 }; //Null entry

	// Initialize table with "default" entries
	for(int i = 1; i < GDT_BASE_ENTRIES; i++) {
		gdt[i] = basicGDTEntry();
	}

	// Define kernel/userspace code segments
	// Note: Default entries work for both data segments, as priviledge levels
	//       are ignored in 64-bit mode data segments
	gdt[GDT_KERNEL_CODE_ARRAY_INDEX].accessByte |= ACCESS_EXECUTABLE_FLAG;
	gdt[GDT_KERNEL_CODE_ARRAY_INDEX].limitPartTwoAndFlags |= LONG_MODE_CODE_FLAG;
	gdt[GDT_USER_CODE_ARRAY_INDEX].accessByte |= ACCESS_EXECUTABLE_FLAG | ACCESS_USERSPACE_FLAG;
	gdt[GDT_USER_CODE_ARRAY_INDEX].limitPartTwoAndFlags |= LONG_MODE_CODE_FLAG;

	// Set up GDTR data
	GDTRegister gdtr;
	gdtr.limit = GDT_BASE_ENTRIES * sizeof(GDTCodeOrDataEntry) - 1;
	gdtr.baseAddress = (UInt64) gdt;

	// Load GDT and jump to new kernel code 'segment'
	asm volatile goto (
		"lgdt %0\n\t"
		"mov %2,%%rax\n\t"
		"mov %%rax,%%ds\n\t"
		"mov %%rax,%%es\n\t"
		"mov %%rax,%%fs\n\t"
		"mov %%rax,%%gs\n\t"
		"mov %%rax,%%ss\n\t"
		"pushq %1\n\t"
		"pushq $%l3\n\t"
		"retfq\n\t"
		: /* No output */
		: "m"(gdtr), "r"(GDT_KERNEL_CODE_SELECTOR_INDEX), "r"(GDT_KERNEL_DATA_SELECTOR_INDEX)
		: "rax", "memory", "cc"
		: finish_lgdt
	);

	finish_lgdt:

	return gdt;
}
Here's the state of the registers at crash time:

Code: Select all

rax=0000000000000010 rbx=0000000000062020 rcx=0000000000000010 rdx=0000000000000008
rsi=0000000000041028 rdi=0000000000000061 r8 =000000000000007d r9 =00000000dfa082ce
r10=0000000000000078 r11=00000000dfa082ce r12=00000000de71f698 r13=0000000000000109
r14=0000000000000000 r15=0000000000000000 iopl=0 nv up di pl nz na pe nc
rip=0000000000402257 rsp=00000000df237830 rbp=00000000df237870
cs=0038 ds=0010 es=0010 fs=0010 gs=0010 ss=0030                     rflags=00000002
%0000000000402257 8e d0                   mov ss, eax
You can see it actually triple faulted right on that mov instruction, and that the other segment registers were set properly. Lastly, here's VirtualBox's dump of my GDT on the crash:

Code: Select all

0008 CodeEO Bas=00000000 Lim=000fffff DPL=0 P  NA       AVL=0 L=1
0010 DataRO Bas=00000000 Lim=000fffff DPL=0 P  A        AVL=0 L=0
0018 CodeEO Bas=00000000 Lim=000fffff DPL=3 P  NA       AVL=0 L=1
0020 DataRO Bas=00000000 Lim=000fffff DPL=0 P  NA       AVL=0 L=0
I believe all the entries look good there. I even set all of the fields that the AMD manuals say are ignored in long mode, just in case. I'm sure the issue is something silly, I'm probably just being blind, but I've spent way too much time trying to figure this out. I'm happy to provide any other info you might need. Thanks ahead of time for any help!
Last edited by Tyyrus on Thu Dec 23, 2021 10:29 am, edited 1 time in total.
linuxyne
Member
Member
Posts: 211
Joined: Sat Jul 02, 2016 7:02 am

Re: Triple Fault on Setting SS Register

Post by linuxyne »

Is the W bit set in the Type field of the GDT descriptor for the data segment?
Tyyrus
Posts: 9
Joined: Tue May 26, 2020 8:44 pm

Re: Triple Fault on Setting SS Register

Post by Tyyrus »

linuxyne wrote:Is the W bit set in the Type field of the GDT descriptor for the data segment?
I tried it both ways and nothing changed, but the AMD64 documentation says it's ignored anyway in long mode:
Segment-limit checking is not performed on any data segments in 64-bit mode, and both the segment-
limit field and granularity (G) bit are ignored. The D/B bit is unused in 64-bit mode

The expand-down (E), writable (W), and accessed (A) type-field attributes are ignored
linuxyne
Member
Member
Posts: 211
Joined: Sat Jul 02, 2016 7:02 am

Re: Triple Fault on Setting SS Register

Post by linuxyne »

Tyyrus wrote:but the AMD64 documentation says it's ignored anyway in long mode
True, but on IvyBridge kvm and on the QEMU's TCG, without the W bit, the machines throw a GPF. QEMU's helper_load_seg function checks for DESC_W_MASK when handling a write into R_SS. QEMU's own bios code also takes care of setting this bit (and a few others) despite it being marked as ignored in the long mode. I haven't tested on bare-metal, though.

Since you are already in 64-bit long mode, and one can see that the SS is set to 0x30 (perhaps by the UEFI), one can compare the bits between that descriptor and the one you are trying to set. This should immediately expose any differences between the two descriptors.

Edit: From the Intel manuals: "Loading the SS register with a segment selector for a nonwritable data segment generates a general-protection exception (#GP)."
Last edited by linuxyne on Thu Dec 23, 2021 9:53 am, edited 1 time in total.
User avatar
iansjack
Member
Member
Posts: 4703
Joined: Sat Mar 31, 2012 3:07 am
Location: Chichester, UK

Re: Triple Fault on Setting SS Register

Post by iansjack »

Is the Segment Present flag in the segment descriptor set?
Tyyrus
Posts: 9
Joined: Tue May 26, 2020 8:44 pm

Re: Triple Fault on Setting SS Register

Post by Tyyrus »

linuxyne wrote: True, but on IvyBridge kvm and on the QEMU's TCG, without the W bit, the machines throw a GPF. QEMU's helper_load_seg function checks for DESC_W_MASK when handling a write into R_SS. QEMU's own bios code also takes care of setting this bit (and a few others) despite it being marked as ignored in the long mode. I haven't tested on bare-metal, though.

Since you are already in 64-bit long mode, and one can see that the SS is set to 0x30 (perhaps by the UEFI), one can compare the bits between that descriptor and the one you are trying to set. This should immediately expose any differences between the two descriptors.

Edit: From the Intel manuals: "Loading the SS register with a segment selector for a nonwritable data segment generates a general-protection exception (#GP)."
Thank you, problem solved after comparing my GDT to the one set by UEFI! It was an issues with the W bit after all. I had tried setting it, but I had mistakenly used the wrong index and set it for the kernel code segment instead. I didn't pay much attention to it, since the AMD documentation said the bit was supposed to be ignored anyway. In the future I'll keep in mind that it might not always be accurate (especially with VMs) and check both that and the Intel manuals. Thanks again!
Post Reply