xv6, line by line
tour 7
Tours7 The trampoline and the trapframe

Tour 7 · Traps and system calls · about 41 minutes · 21 steps

The trampoline and the trapframe

Tour 5: Life of a system call crossed the user/kernel border twice and moved on quickly. This tour stays at the border. It is 83 instructions long, written in assembly, and runs while the hart belongs to nobody: no longer the user program, not yet the kernel. It owns no stack, no free register, and for part of the time no page table that maps what it needs.

We follow the first process, init (pid 1), through the first border crossings the machine ever makes: out of the kernel into user mode for the very first time, then back in when init makes its first system call, open("console", O_RDWR). Every number on the way comes from this build and from a gdb session on QEMU with three harts: the satp values, the page-table entries, the physical address of init’s trapframe, even the junk left in its registers. (Our run used an fs.img that had been booted before, so console already existed; on a brand-new image the first open fails, and init runs mknod("console", …) and opens it again.)

Two ideas carry the whole design. The trampoline page is one physical page with the same virtual address in every page table, so the code survives the moment satp changes under it. The trapframe is one page per process at the same virtual address in every user page table, so the same instructions save each process’s registers into that process’s own page. At the end, three harts run the trampoline at the same instant, and you will see why they need no lock.

Best after: 5. Life of a system call

Who is running where

The machine has three harts. When the tour starts, hart 0 is finishing boot in main and harts 1 and 2 are spinning, waiting for it to set started. Later:

Hart What it is doing
0 Builds the kernel, creates init; later returns init to user mode and takes its first trap
1 Its scheduler loops, finding nothing else to run
2 Its scheduler picks init first, starts it in forkret, and loses it when it sleeps on the disk

In our gdb run, init started in the kernel on hart 2, slept while fsinit read the disk, and woke up on hart 0. Which hart wins is a race; the rest of the story does not depend on it.

Three harts are running. This tour follows one path through the code, but the machine has three CPUs executing at the same time. Watch the locks held display at the top of each step, and read the Meanwhile, on other harts boxes: they show what the other CPUs could be doing at that very moment.
The route
  1. 1One page, two virtual addresses in the kernel kernel/vm.c
  2. 2The same page in init's page table, plus a private one kernel/proc.c
  3. 3The trapframe, field by field kernel/proc.h
  4. 4init's first trip out begins in forkret kernel/proc.c
  5. 5prepare_return: point stvec at the trampoline, with interrupts off kernel/trap.c
  6. 6prepare_return: the message to the next trap kernel/trap.c
  7. 7prepare_return: arm sret kernel/trap.c
  8. 8Into the trampoline at its high address kernel/trampoline.S
  9. 9Switching to init's page table kernel/trampoline.S
  10. 10li a0, TRAPFRAME: an absolute address kernel/trampoline.S
  11. 11ld sp: init's user stack, and the rest of its registers kernel/trampoline.S
  12. 12sret, and init is alive kernel/trampoline.S
  13. 13init traps: open("console") executes ecall user/usys.S
  14. 14sscratch buys one free register kernel/trampoline.S
  15. 15Thirty-one stores into init's page kernel/trampoline.S
  16. 16Picking up the kernel's message kernel/trampoline.S
  17. 17The floor changes, the code keeps running kernel/trampoline.S
  18. 18jalr t0 and the return address that is userret kernel/trampoline.S
  19. 19usertrap hands back the user satp kernel/trap.c
  20. 20Three harts in the trampoline at once kernel/trampoline.S
  21. 21What the border costs, and why it is built this way user/init.c

Keys: ← → step · Home start