Page 1 of 1

[SOLVED] Triple Fault on Setting SS Register

Posted: Thu Dec 23, 2021 7:19 am
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!

Re: Triple Fault on Setting SS Register

Posted: Thu Dec 23, 2021 9:08 am
by linuxyne
Is the W bit set in the Type field of the GDT descriptor for the data segment?

Re: Triple Fault on Setting SS Register

Posted: Thu Dec 23, 2021 9:18 am
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

Re: Triple Fault on Setting SS Register

Posted: Thu Dec 23, 2021 9:33 am
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)."

Re: Triple Fault on Setting SS Register

Posted: Thu Dec 23, 2021 9:53 am
by iansjack
Is the Segment Present flag in the segment descriptor set?

Re: Triple Fault on Setting SS Register

Posted: Thu Dec 23, 2021 10:28 am
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!