The kernel runs in ring0, which has the most permissions on the cpu. We want user programs to run in ring 3, which has the least.

Once we have a user program loaded into memory where it wants to be, which is done here, we need to somehow start running code there but in ring 3, so we can’t just call _start.

In the beginning, we initialized the global descriptor table with some information about segments. There are code and data segments for kernel mode, ring 0, as well as code and data segments for ring 3.

0x00CF9A000000FFFF is what is loaded for the kernel data segment. The first two bytes are the limit of the segment, which in combination with the page granularity bit means that the limit is the entire address space (since we don’t care about segmentation at all). For the same reason, the base address is 0. The parts we actually care about are the access byte and flags. Flags really just say that we want the whole address space still and in 32 bit mode. The access byte specifies whether the segments is present, which it is. The next two bits are the privilege level. This is where you specify which of the 4 privilege levels you want. Then you can specify whether this is a code/data segment, or a system segment. After that there is a bit to determine if the segment is executable.

To get into ring 3, we trick the processor into thinking it was already there. x86 cpu’s are very advanced but also very dumb, and will do exactly what we tell them. When an interrupt happens, the CPU pushes some stuff to the stack so that it knows where to go once the interrupt is over, and an iret is called.

The processor doesn’t actually know the difference between the stuff that it pushed to the stack and the stuff that we pushed to the stack, so we will just pretend that an interrupt happened and we want to go back.

To do this, we push the stack segment, the stack pointer, the flags, the code segment, and the “return address”. Of course, these values are all new. The segments are our user segment selectors, and the stack pointer is a new stack pointer for the user code. The “return” address is actually the entry point of our program. Once all that is pushed to the stack, iret will restore the processor to the state it thinks that it was in, which is ring3, at the entry point of the user program. When we exit the program, the syscall for exit will send us back into ring0.