Back to bookshelf

Preemptive threads, checked with single step equivalence | CS 140E 

Appreciation
7
Importance
--
Date Added
3.1.26
TLDR
Implementing preemptive context switching by saving and restoring all 17 registers (r0–r15 + CPSR) to forcibly switch between threads. Verifying the restore works using a debug hardware mismatch fault (since there's no easy way to inspect register values after resumption).
2 Cents
Really informative lab that tied together the previous labs for cooperative threading and interrupts. Clicked this time.
Tags

We are enabling preemptive context switching, the ability to:

  1. forcibly interrupt a running process
  2. save its entire state (all 17 registers: r0–r15 + CPSR)
  3. switch to another process by restoring that process's 17 registers
  4. resume it exactly where it left off.

i.e., how any OS shares a CPU between programs.

We verify step 4 using a debug hardware trick (triggering a mismatch fault). Without this, verification would be incredibly hard.

#0. Prereq: caller-saved and callee-saved

The ARM calling convention splits registers into two groups:

  • caller-saved (r0–r3, r12, lr) which any function is free to trash, so the caller must save them if it cares
  • callee-saved (r4–r11, sp) which a function promises to leave untouched, so the callee saves them before using them

Because this contract only works when code voluntarily calls a function, we cannot rely on it for context switching.

#1. 3 types of context switching

  1. Interrupts: The process didn't choose to call anything as “we” ripped control away. So we save caller-saved registers for it (it never got the chance). Callee-saved are fine because our C handler obeys the convention. We return to the same code afterward.
  2. Cooperative threading (threads voluntarily yield control): The process calls yield(), so the compiler handles caller-saved registers as with any function call. But yield() actually switches to a different thread, which will use r4–r11 for its own purposes, so we must save callee-saved registers across the switch.
  3. Preemptive threading (threads are forcibly interrupted and switched): Both problems at once. The process didn't choose to call anything (save caller-saved), and we're switching threads (save callee-saved). Need to save all 17 registers!

#2. The debug hardware trick

To verify that preemptive threads work, we need to check that saving and restoring the 17 registers work.

  • Saving is easy enough because we can inspect the saved values in memory.
  • Restoring is harder: once you restore and resume, the code immediately starts modifying registers so you can't check they were correct before execution changed them.

The solution is:

  • ARM debug hardware can trigger a mismatch fault (“fault when PC != address X.”)
  • Set X to an address you'll never jump to, call the restore code, and the instant the CPU arrives at the restored PC, the mismatch fires before executing that instruction
  • We are back in the fault handler with the kernel's stack, and the handler's trampoline saves all the registers as they were right after restore. Now we can compare them and verify it worked!