Search code examples
assemblyx86operating-systemkernelgnu-assembler

Use jmp instruction to a non-constant TSS segment


JMP instruction referece.

According to the documentation, we can perform jmp to a constant far segment:

jmp 0x18:00

Here, 0x18 is a valid segment selector in a GDT, Global Descriptor Table.

jmp can be used with a segment register that contains a valid GDT entry, that is a code/data segment descriptor:

mov es, 0x18
jmp es:0x0

Here, 0x18 is a TSS (Task State Segment) descriptor that when jumping to, CPU performs a task switch that automatically saves its state into the current TSS, and then populate with the state saved in the new TSS.

However, TSS is a system segment descriptor, and therefore cannot be loaded into any of the segment register (as suggested by Intel document). Then how, can I jump to a task at runtime with dynamically allocated TSS?

The only way I can think of is to use the iret instruction, but I feel it like a hack, as I need to modify the link field, and then set the NT bit in EFLAGS to perform back link task switching.


Solution

  • Not only can't you load ES with a TSS selector, the instruction jmp es:0x0 is also invalid. There's no instruction that moves a segment register to another segment register (eg. ES to CS). There's also no instruction that will load CS from a general register. As Margaret Bloom's answer shows you'll need load CS with JMP instruction that takes a memory operand, specifically one that takes a far pointer as a memory operand so you get a far jump instruction that sets CS.

    As far as implementing this it would make sense to put this far pointer in your task structure, the structure where you place the task's TSS and other task specific information. For example to switch tasks you could use code like this:

    struct task {
        struct {
            unsigned offset;
            unsigned short selector;
        } far_jmp_ptr;
        struct tss tss;
        // ...
    };
    
    void
    switch_tasks(struct task *new_task) {
        asm("jmp FAR PTR %0" : : "m" (new_task->far_jmp_ptr));
    }
    

    The code assumes a "task structure" with a far pointer that contains the allocated TSS selector for the task (the offset part is ignored).

    Technically, you can also jump to a task by using an LTR instruction followed by a JMP instruction. This changes the task without performing a task switch, so no registers (other than TR, CS:EIP and any other registers you explicitly change) are affected. For example:

    mov  esi, [new_task]
    ltr  [esi + TASK_FAR_JMP_PTR + 4]
    jmp  [esi + TASK_TSS + TSS_EIP]
    

    This would only be practical if the new task is running at ring 0 and either is just starting or was stopped at a known point where it didn't need its registers restored. In particular this is how you might start an initial kernel task (or the only task in a single TSS operating system.)

    Note that most operating systems only use one TSS for all tasks, and so don't use the task switching mechanism provided by the CPU. For 64-bit operating systems this is required, as task switching isn't supported in long mode.