Demilade Sonuga's blog

All posts

The Task State Segment

2023-02-07 · 51 min read

The next step on our list before we can get back to interrupts is to set up a Task State Segment.

TSS Structure

The structure of the TSS:

Bytes Meaning
0..=3 Reserved
4..=27 The privilege stack table
28..=35 Reserved
36..=91 The Interrupt Stack Table
92..=99 Reserved
100..=101 Reserved
102..=103 IO map base address

The privilege stack table is an array of three 64-bit addresses used to switch stacks when a privilege level change occurs. In a normal system, the OS and a user program cannot use the same stack, for security purposes. In this project, there are no user programs, so we don't have to worry about this field.

The Interrupt Stack Table is an array of seven 64-bit addresses used to load a stack when an interrupt handler warrants it. Remember from the first IDT post that bits 0..=2 of the options field in an Interrupt Descriptor Table entry is an offset into the Interrupt Stack Table. This offset is an index into this array, telling which stack should be used while handling the interrupt.

A Few Things About Stacks

If you aren't too familiar with all this stack talk, all you need to know is that a stack is a chunk of memory in which writing data and removing data takes place at one end. Functions use this memory to store their local variables.

Consider this:

fn func1()
    var1: byte = 2
    func2()

fn func2()
    var2: byte = 4
    func3()

fn func3()
    var3: byte = 10000

Prior to the execution of func1, the area of memory designated as the stack will look like this:

1000: 0 <- pointer
1001: 0
1002: 0
...

The above assumes that the stack starts at address 1000. There is always a pointer to indicate where on the stack the next value can be placed.

When func1's execution reaches the call of func2, the stack will look like this:

1000: 2
1001: 0 <- pointer
1002: 0
...

When func2 is called, it saves its local variables on the stack, too. The stack becomes this before func3 starts executing:

1000: 2
1001: 4
1002: 0 <- pointer
1003: 0
...

On func3 execution:

1000: 2
1001: 4
1002: 10000
1003: 0 <- pointer
...

When func3 is done executing and returns:

1000: 2
1001: 4
1002: 10000 <- pointer
1003: 0
...

The pointer indicates that the memory location that was used to store func3's variables can now be used for other functions.

When func2 returns:

1000: 2
1001: 4 <- pointer
1002: 10000
...

And finally, func1 returns:

1000: 2 <- pointer
1001: 4
1002: 10000

The values that were placed on the stack are still there and will remain there until some other function call overwrites them with its own local variables.

Anyways, this is still just normal memory, but values are being added and "removed" from only one end.

Back To The TSS Structure

The previous section about stacks also explains why different stacks may be needed for different privilege levels, because the values are never actually removed until they are overwritten. A lesser privilege program using the same stack as the OS could lead to that lesser privilege program obtaining sensitive data from the OS's stack.

The last thing to look at in the TSS is the IO base map address. This is a 16-bit offset from the starting address of the TSS to a structure called the IO permission bit map. IO ports are used to connect the computer to some peripheral device or other hardware interfaces. User programs may need to access these ports but there has to be some way to determine whether or not a program is allowed to do that. The mechanism the processor uses to do this is the IO permission bit map. This structure is used to verify whether or not a user-mode program is allowed to access an IO port. Again, this is something we don't need to think about much because there will be no user mode programs in our project.

If this IO base map address field has a value greater than or equal to the TSS size, then there is no IO permission map, which is actually what we want here since we won't be needing it.

Modeling The TSS

Using the information given, model the TSS yourself before moving on.

First, create a new file tss.rs and throw this in:

// The Task State Segment
#[repr(C, packed)]
pub struct TSS {
reserved1: u32,
// Stack pointers for different privilege levels
privilege_stack_table: [*mut u8; 3],
reserved2: u64,
// Stack pointers for switching stacks when an interrupt handler wants to
interrupt_stack_table: [*mut u8; 7],
reserved3: u64,
reserved4: u16,
// Offset from the TSS start address to the IO permission bit map
io_map_base_addr: u16
}

A constructor function to create a new TSS:

impl TSS {
// Creates a new TSS with all the stack pointers set to null pointers
// and no IO permission bit map
pub fn new() -> Self {
Self {
privilege_stack_table: [core::ptr::null_mut(); 3],
interrupt_stack_table: [core::ptr::null_mut(); 7],
// Setting this field to the size of the TSS indicates that
// there is no IO permission bit map
io_map_base_addr: core::mem::size_of::<TSS>() as u16,
// Always better to leave the reserved fields as 0s
reserved1: 0,
reserved2: 0,
reserved3: 0,
reserved4: 0
}
}
}

All the stack pointers are initialized to null pointers with the core::ptr::null_mut.

Creating A TSS

In main.rs:

// ...Others

mod gdt;
use gdt::{Descriptor, GDT, SegmentRegister};

// NEW:
mod tss;
use tss::TSS;

mod interrupts;
mod game;

static mut SCREEN: Option<&mut Screen> = None;
static mut GDT: Option<GDT> = None;
static mut TSS: Option<TSS> = None; // NEW

#[no_mangle]
extern "efiapi" fn efi_main(
handle: *const core::ffi::c_void,
sys_table: *mut SystemTable,
) -> usize {
// ...Others

boot_services.exit_boot_services(handle).unwrap();

setup_gdt();
setup_tss();
}

// ...Others

// Creates a new TSS and sets it up
fn setup_tss() {
let tss: &mut TSS;
unsafe {
TSS = Some(TSS::new());
tss = TSS.as_mut().unwrap();
}
}

// ...Others

At this point, all we need to do is to tell the processor where the TSS is. The mechanism used to tell which code and data segments are being used is almost the same mechanism used for this. Firstly, we need to enter a system descriptor into the GDT describing our TSS, then we retrieve the segment selector of the TSS.

This segment selector is then loaded into a register called the task register with a special instruction, ltr. The use of this special instruction makes this process similar to the way we load the GDT.

Creating A New Descriptor

Before proceeding, create a new tss_segment function associated with Descriptor in gdt.rs yourself that takes a reference to a TSS and returns its system descriptor. You can use the first GDT post's description of the structure of a system segment descriptor.

To create the segment descriptor:

In gdt.rs

use core::mem;
use core::arch::asm;
use crate::tss::TSS; // NEW

// ...Others

impl Descriptor {
pub fn code_segment() -> Self { /* ...Others */ }

pub fn data_segment() -> Self { /* ...Others */ }

pub fn tss_segment(tss: &'static TSS) -> Self {
let mut upper_descriptor_value = 0u64;
let mut lower_descriptor_value = 0u64;
}
}

Using the first GDT post's description as our guide:

The first 16 bits of the first part of the descriptor are the lower 16 bits of the limit. The limit is the size of the structure - 1. So, we have to set the lower 16 bits of the descriptor to the lower 16 bits of the limit.

To do this, we have to get into another bitwise operator: bitwise and.

Bitwise And

Remember from the third GDT post that the bitwise OR takes 2 numbers, treats binary 0s and 1s as false and true values respectively and performs the OR operation on the numbers' bits whose positions correspond.

The bitwise AND (&) is just like the bitwise OR except for one thing: instead of performing the OR operation, it performs AND. In case you're not aware, the logical AND operation is defined as:

First Digit Second Digit Output
0 0 0
1 0 0
0 1 0
1 1 1

The output is 1 only if the two digits are 1s. Or, logically, the output is true only if both inputs are true.

For multiple bits, the bitwise AND of 1 & 2 == 0b00000001 & 0b00000010 is:

Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
First Number 0 0 0 0 0 0 0 1
Second Number 0 0 0 0 0 0 1 0
Output 0 0 0 0 0 0 0 0

The output is 0 because there are no corresponding positions whose bits are both 1.

This operation is relevant to us because we can use it to set and clear bits of a number to produce a new number.

For example, consider the number 6 whose binary equivalent is 0b110. If we want to set bit 1 to 0, we can do this:

fn and() {
let six = 7;
let bitmask = 0b101;
let four = six & bitmask;
}

That is, create a value whose bits are all 1s except the points where you want a 0. Then you do a bitwise AND with this value, called a bitmask. The points where the bitmask is 0 are guaranteed to be 0 after the operation because AND will only output 1 if both values are 1. At points where the bitmask has values of 1, the output number will only have 1s if the original number had them. To make this more concrete:

Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
Six 0 0 0 0 0 1 1 0
Bitmask 0 0 0 0 0 1 0 1
Output 0 0 0 0 0 1 0 0

Bit 1 of the output is 0 because the bitmask's bit 1 is 0. Bit 0 of the output is 0 even though the bitmask's bit 0 is 1 because the number's bit 0 is 0. This just shows how the bitwise AND doesn't change the bits at points where the bitmask's bit is 1. So, the bitwise AND will be good for unsetting bits at points where we want them to be unset (equal 0).

Back To Creating A New Descriptor

The first 16 bits of the first part of the system descriptor must be the first 16 bits of the limit. To retrieve the first 16 bits of the limit, we have to 0 out all the other bits which are not in the lower 16 and this can be done with a bitwise AND.

impl Descriptor {
// ...Others

pub fn tss_segment(tss: &'static TSS) -> Self {
let mut upper_descriptor_value = 0u64;
let mut lower_descriptor_value = 0u64;

// NEW:
let limit = mem::size_of::<TSS>() - 1;
// Retaining only the lower 16 bits of the limit
let limit_lower_16_bits = limit & 0xffff;
}
}

Remember that a single hex digit is 4 bits and 0xf is all 1s, so 0xffff is a number with all 1s as its lower 16 bits.

Now, we want to set this limit_lower_16_bits to become the lower 16 bits of lower_descriptor_value. This can be done with a simple bitwise OR operation:

impl Descriptor {
// ...Others

pub fn tss_segment(tss: &'static TSS) -> Self {
let mut upper_descriptor_value = 0u64;
let mut lower_descriptor_value = 0u64;

let limit = mem::size_of::<TSS>() - 1;
let limit_lower_16_bits = limit & 0xffff;
// Setting the lower 16 bits of the limit to the lower 16 bits of the descriptor
lower_descriptor_value = lower_descriptor_value | (limit_lower_16_bits as u64); // NEW
}
}

Bits 16..=39 of the descriptor are the lower 24 bits of the base, the starting address.

impl Descriptor {
// ...Others

pub fn tss_segment(tss: &'static TSS) -> Self {
let mut upper_descriptor_value = 0u64;
let mut lower_descriptor_value = 0u64;

let limit = mem::size_of::<TSS>() - 1;
let limit_lower_16_bits = limit & 0xffff;
lower_descriptor_value = lower_descriptor_value | (limit_lower_16_bits as u64);

let base = tss as *const _ as u64; // NEW
}
}

Remember that references are just addresses. The tss reference passed as the argument is the address of the TSS that we're describing here. To get the lower 24 bits:

impl Descriptor {
// ...Others

pub fn tss_segment(tss: &'static TSS) -> Self {
let mut upper_descriptor_value = 0u64;
let mut lower_descriptor_value = 0u64;

let limit = mem::size_of::<TSS>() - 1;
let limit_lower_16_bits = limit & 0xffff;
lower_descriptor_value = lower_descriptor_value | (limit_lower_16_bits as u64);

let base = tss as *const _ as u64;
// Retaining only the lower 24 bits of the base
let base_lower_24_bits = base & 0xffffff; // NEW
}
}

To set bits 16..=39 of lower_descriptor_value to base_lower_24_bits, we use a left shift operation and a bitwise OR:

impl Descriptor {
// ...Others

pub fn tss_segment(tss: &'static TSS) -> Self {
let mut upper_descriptor_value = 0u64;
let mut lower_descriptor_value = 0u64;

let limit = mem::size_of::<TSS>() - 1;
let limit_lower_16_bits = limit & 0xffff;
lower_descriptor_value = lower_descriptor_value | (limit_lower_16_bits as u64);

let base = tss as *const _ as u64;
let base_lower_24_bits = base & 0xffffff;
// Setting bits 16..=39 of the descriptor to the lower 24 bits of the base
lower_descriptor_value = lower_descriptor_value | (base_lower_24_bits << 16); // NEW
}
}

This works because the base_lower_24_bits << 16 produces a new number whose bits 16..=39 is base_lower_24_bits and everything else is 0. This value bitwise ORed with lower_descriptor_value will set its bits 16..=39 to the base_lower_24_bits while retaining the other bits we've already set.

Bits 40..=47 is the access byte of the descriptor. In the system segment descriptor, the access byte's first four bits tell what type of system descriptor is being described. In our case, this must be 0b1001, the binary equivalent of 9. According to the Intel SDA manual, this value means the system descriptor is for a TSS.

impl Descriptor {
// ...Others

pub fn tss_segment(tss: &'static TSS) -> Self {
let mut upper_descriptor_value = 0u64;
let mut lower_descriptor_value = 0u64;

let limit = mem::size_of::<TSS>() - 1;
let limit_lower_16_bits = limit & 0xffff;
lower_descriptor_value = lower_descriptor_value | (limit_lower_16_bits as u64);

let base = tss as *const _ as u64;
let base_lower_24_bits = base & 0xffffff;
lower_descriptor_value = lower_descriptor_value | (base_lower_24_bits << 16);

// NEW:
let mut access_byte = 0u8;
// Setting the first 4 bits of the access byte to 1001, the indicator
// of a TSS
access_byte = access_byte | 0b1001;
}
}

Bit 4 must be 0, to indicate that the segment is a system segment. We don't have to do anything now because bit 4 is already 0. Bits 5..=6 should also be 0b00 to indicate that the segment's privilege level is the highest and it's already 0b00.

Bit 7 should be 1, to indicate that the segment is present and valid.

impl Descriptor {
// ...Others

pub fn tss_segment(tss: &'static TSS) -> Self {
let mut upper_descriptor_value = 0u64;
let mut lower_descriptor_value = 0u64;

let limit = mem::size_of::<TSS>() - 1;
let limit_lower_16_bits = limit & 0xffff;
lower_descriptor_value = lower_descriptor_value | (limit_lower_16_bits as u64);

let base = tss as *const _ as u64;
let base_lower_24_bits = base & 0xffffff;
lower_descriptor_value = lower_descriptor_value | (base_lower_24_bits << 16);

let mut access_byte = 0u8;
access_byte = access_byte | 0b1001;
// Setting bit 7 of the access byte
access_byte = access_byte | (1 << 7); // NEW
}
}

1 << 7 creates a new value whose bit 7 is 1. ORing this new value with access_byte sets its bit 7.

The access byte itself is bits 40..=47:

impl Descriptor {
// ...Others

pub fn tss_segment(tss: &'static TSS) -> Self {
let mut upper_descriptor_value = 0u64;
let mut lower_descriptor_value = 0u64;

let limit = mem::size_of::<TSS>() - 1;
let limit_lower_16_bits = limit & 0xffff;
lower_descriptor_value = lower_descriptor_value | (limit_lower_16_bits as u64);

let base = tss as *const _ as u64;
let base_lower_24_bits = base & 0xffffff;
lower_descriptor_value = lower_descriptor_value | (base_lower_24_bits << 16);

let mut access_byte = 0u8;
access_byte = access_byte | 0b1001;
access_byte = access_byte | (1 << 7);
// Setting the access byte to bits 40..=47 of the descriptor
lower_descriptor_value = lower_descriptor_value | ((access_byte as u64) << 40); // NEW
}
}

Bits 48..=51 are the upper 4 bits of the limit:

impl Descriptor {
// ...Others

pub fn tss_segment(tss: &'static TSS) -> Self {
let mut upper_descriptor_value = 0u64;
let mut lower_descriptor_value = 0u64;

let limit = mem::size_of::<TSS>() - 1;
let limit_lower_16_bits = limit & 0xffff;
lower_descriptor_value = lower_descriptor_value | (limit_lower_16_bits as u64);

let base = tss as *const _ as u64;
let base_lower_24_bits = base & 0xffffff;
lower_descriptor_value = lower_descriptor_value | (base_lower_24_bits << 16);

let mut access_byte = 0u8;
access_byte = access_byte | 0b1001;
access_byte = access_byte | (1 << 7);
lower_descriptor_value = lower_descriptor_value | ((access_byte as u64) << 40);

// NEW:
// Shifting out the lower 16 bits of the limit and retaining only the upper 4 bits
let limit_upper_4_bits = (limit >> 16) & 0xf;
// Setting the upper 4 bits of the limit to bit 48..=51 of the descriptor
lower_descriptor_value = lower_descriptor_value | ((limit_upper_4_bits as u64) << 48);
}
}

Remember that the limit is supposed to be a 20-bit value. To get the upper 4 bits of it, it's first right shifted to get rid of the lower 16 bits. The limit >> 16 produces a new value whose lower 4 bits are the upper 4 bits of limit. Bitwise ANDing this new value with 0xf turns all bits that are not in the lower 4 bits to 0s.

Bit 53 tells whether the segment is a 64-bit code segment, which it is not. So, this is left as 0.

Bit 54 is 0 for a 64-bit segment, so we leave this as 0.

Bit 55 is the granularity, telling if the limit is in 1-byte units or 4-Kib units. It should be 1 for 4Kib units:

impl Descriptor {
// ...Others

pub fn tss_segment(tss: &'static TSS) -> Self {
let mut upper_descriptor_value = 0u64;
let mut lower_descriptor_value = 0u64;

let limit = mem::size_of::<TSS>() - 1;
let limit_lower_16_bits = limit & 0xffff;
lower_descriptor_value = lower_descriptor_value | (limit_lower_16_bits as u64);

let base = tss as *const _ as u64;
let base_lower_24_bits = base & 0xffffff;
lower_descriptor_value = lower_descriptor_value | (base_lower_24_bits << 16);

let mut access_byte = 0u8;
access_byte = access_byte | 0b1001;
access_byte = access_byte | (1 << 7);
lower_descriptor_value = lower_descriptor_value | ((access_byte as u64) << 40);

let limit_upper_4_bits = (limit >> 16) & 0xf;
lower_descriptor_value = lower_descriptor_value | ((limit_upper_4_bits as u64) << 48);

// Setting bit 55 of the descriptor for a granularity of 1-Kib units
lower_descriptor_value = lower_descriptor_value | (1 << 55); // NEW
}
}

Bits 56..=95 are the upper 40 bits of the base. It cuts across the two parts of the descriptor. To accommodate for this split, we can set the remaining upper 8 bits of the lower part of the descriptor to the first 8 bits of this upper 40 bits of the base. The remaining 32 bits will be set to the lower 32 bits of the second part of the descriptor.

impl Descriptor {
// ...Others

pub fn tss_segment(tss: &'static TSS) -> Self {
let mut upper_descriptor_value = 0u64;
let mut lower_descriptor_value = 0u64;

let limit = mem::size_of::<TSS>() - 1;
let limit_lower_16_bits = limit & 0xffff;
lower_descriptor_value = lower_descriptor_value | (limit_lower_16_bits as u64);

let base = tss as *const _ as u64;
let base_lower_24_bits = base & 0xffffff;
lower_descriptor_value = lower_descriptor_value | (base_lower_24_bits << 16);

let mut access_byte = 0u8;
access_byte = access_byte | 0b1001;
access_byte = access_byte | (1 << 7);
lower_descriptor_value = lower_descriptor_value | ((access_byte as u64) << 40);

let limit_upper_4_bits = (limit >> 16) & 0xf;
lower_descriptor_value = lower_descriptor_value | ((limit_upper_4_bits as u64) << 48);

lower_descriptor_value = lower_descriptor_value | (1 << 55);

// NEW:
// Shifting out the lower 24 bits and retaining only the upper 40
let base_upper_40_bits = (base >> 24) & 0xffffffffff;
// Retaining only the lower 8 bits of the base's upper 40 bits
let base_lower_8_of_upper_40 = base_upper_40_bits & 0xff;
// Shifting out the lower 8 bits and
// retaining only the upper 32 bits of the base's upper 40 bits
let base_upper_32_of_upper_40 = (base_upper_40_bits >> 8) & 0xffffffff;
// Setting the lower 8 bits of the base's upper 40 bits to bits 56..=63 of the descrtiptor
lower_descriptor_value = lower_descriptor_value | (base_lower_8_of_upper_40 << 56);
// Setting the upper 32 bits of the base's upper 40 bits to bits 64..=95 of the descriptor
upper_descriptor_value = upper_descriptor_value | base_upper_32_of_upper_40;

Self::System(upper_descriptor_value, lower_descriptor_value)
}
}

Setting Up The TSS

Now, all that's left to do is to add the descriptor to the GDT and set the task register to the descriptor's segment selector with the ltr instruction.

In main.rs

// Setups up a new TSS
fn setup_tss() {
let tss: &mut TSS;
unsafe {
TSS = Some(TSS::new());
tss = TSS.as_mut().unwrap();
}
let gdt = unsafe { GDT.as_mut().unwrap() };
let tss_selector = gdt.add_descriptor(Descriptor::tss_segment(tss)).unwrap();
load_tss(tss_selector);
}

To load the TSS, we'll handle the assembly in a load_tss function.

In tss.rs

use core::arch::asm;
use crate::gdt::SegmentSelector;

// ... Others

pub fn load_tss(sel: SegmentSelector) {
unsafe {
asm!("ltr {}", in(reg) sel.0);
}
}

In main.rs

// ...Others

mod gdt;
use gdt::{Descriptor, GDT, SegmentRegister};

mod tss;
// DELETED: use tss::TSS;
use tss::{TSS, load_tss}; // NEW

// ...Others

Compiling will give an error that the field 0 of SegmentSelector is private. We resolve this like:

#[derive(Clone, Copy)]
#[repr(transparent)]
// DELETED: pub struct SegmentSelector(u16);
pub struct SegmentSelector(pub u16); // NEW

Compiling now will give no errors.

Take Away

  • A stack is just a chunk of memory in which data is added and removed from only one end.
  • The Task State Segment is used to hold stack pointers for different privilege levels and for interrupt handlers.
  • The Task State Segment is loaded by setting the task register to its segment selector with the ltr instruction.

For the full code, go to the repo

In The Next Post

We'll get on to the Interrupt Descriptor Table

References