The i386 provides a hardware data structure called the task state segment (TSS), which keeps track of segment selectors, general purpose registers, 4 levels of stack pointers, the instruction pointer, and various permissions and flags.
After the task and TSS have been created and all the relevant fields are filled in correctly, you need to create a GDT entry for the TSS, and from there you can switch to the new context at will.
struct TSS32 {
unsigned short backlink, __blh;
unsigned long esp0;
unsigned short ss0, __ss0h;
unsigned long esp1;
unsigned short ss1, __ss1h;
unsigned long esp2;
unsigned short ss2, __ss2h;
unsigned long cr3, eip, eflags;
unsigned long eax,ecx,edx,ebx,esp,ebp,esi,edi;
unsigned short es, __esh;
unsigned short cs, __csh;
unsigned short ss, __ssh;
unsigned short ds, __dsh;
unsigned short fs, __fsh;
unsigned short gs, __gsh;
unsigned short ldt, __ldth;
unsigned short trace, iomapbase;
};
The fields of the TSS should reflect the initial state of the registers when the context is started. Most of the registers can be left alone for the application's use; however, the following fields must be filled in before you start the task initially:
A TSS needs to be added to the GDT before anything context switching can ever occur. A TSS entry (type 9) looks just like every other GDT entry, with appropriate base, limit, and protection values. It is your choice if you want to dynamically or statically allocate entries in the GDT for tasks; however, if you choose to statically allocate TSS entries (they persist beyond a single context switch), you will most likely need to clear the Busy Bit (a Busy TSS' is type 11, an Available TSS is type 9).
The ltr() function will load the current Task Register (TR), thereby setting the current task. Because every context switch both stores the old set of register values and loads in a new set, the TR needs to be loaded before the first task switch occurs. And because each task switch loads the TR with a new value, you will probably not need to touch it again once you start switching.
void ltr(unsigned int selector)
{
asm ("ltr %0": :"r" (selector));
}
unsigned int str(void)
{
unsigned int selector;
asm ("str %0":"=r" (selector));
return selector;
}
A far jump to a TSS selector (regardless of offset) will cause a task switch:
void switch(unsigned int selector)
{
unsigned int sel[2];
/* sel[0] = offset, which is irrelevant for a task switch */
sel[1] = selector;
/* NOTE: lcall sets eFlags[NT] bit, whereas ljmp does not */
asm ("lcall %0": :"m" (*sel));
/* we get back to here when someone else has switch()'ed
back to us */
}
When a context switch occurs, either manually (via the lcall instruction as above) or because of an exception, the processor sets the Nested Task (NT) bit in the eFlags register, sets the Busy (B) Bit (B) in the type field of its GDT entry, stores the selector of the previous task in the back-link field of the called TSS, and transfers control to the next task.
So if the called task executes an iret
instruction with
the eFlags[NT] bit set, the processor assumes that it should return to
the calling task (which is conveniently stored in the backlink field).
This is very useful, in a few instances:
void taskReturn(void)
{
/* asm ("pushf; orl $0x00004000, (%esp); popf; iret"); sets NT */
/* asm ("pushf; andl $0xffffbfff, (%esp); popf; iret"); clears NT */
asm ("iret"); /* assumes the processor will be correct */
}
NOTE: a context switch due to an ljmp
instruction does
not set eFlags[NT], nor change the value of the backlink field.