1Where does a stack that grows have to live?
In this tree kexec places a guard page and one stack page (USERSTACK is 1) right after
the program image, and the heap grows from just above them. Recursion that needs more than
4 KiB dies. In this layout, where could a second stack page come from? Where would you put
the stack instead, in which direction does it grow, what stops it, and what does your
choice do to p->sz, which today covers everything a process owns?
Draw the user address space from 0 to MAXVA using kernel/exec.c:86-kernel/exec.c:97,
the comment at kernel/memlayout.h:54 and the heap’s ceiling in growproc
(kernel/proc.c:243). A stack grows toward lower addresses: what lies directly below
the stack page today, and what lies directly above it?
Two regions that both grow must grow away from each other into free space, with something
between them that neither can take. The heap is [0, p->sz) and grows up. Nothing lives
between the top of the heap and the trapframe. And a region whose bounds are constants is
the same for every process: no new per-process state.
Fix the stack’s top at a constant high address, give the stack a fixed maximum size,
keep one unmapped page below the lowest page the stack may reach, and lower the heap’s
ceiling to just below that page. p->sz then describes the image and the heap only.
The reference design
In the original layout the stack is boxed in. Below its page is the guard page, a real
page that uvmclear made inaccessible to user mode (kernel/exec.c:95), and below that
the program’s data. Above it is the first page of the heap (kernel/exec.c:96 sets the
initial sp to sz, which is also where sbrk starts). The stack cannot grow down
without overwriting data, and it cannot move up because the heap is there.
The reference moves the stack to the top of user memory, where nothing lives:
| address | what |
|---|---|
0x3ffffff000 |
TRAMPOLINE (not user-accessible) |
0x3fffffe000 |
TRAPFRAME (not user-accessible); also USTACKTOP, the stack’s top (exclusive) |
0x3ffffbe000 to 0x3fffffdfff |
the stack region: 64 pages, grown from the top down as needed (USTACKBASE is its bottom) |
0x3ffffbd000 to 0x3ffffbdfff |
the guard page, never mapped |
up to 0x3ffffbd000 |
MAXHEAP, the highest p->sz the heap may reach |
from 0 |
text, data, bss, then the heap, growing up |
Three constants describe it, in memlayout.h: USTACKTOP (= TRAPFRAME), USTACKBASE
(USTACKTOP - USERSTACK * PGSIZE) and MAXHEAP (USTACKBASE - PGSIZE). USERSTACK
keeps its name but changes its meaning, from “the stack’s size” to “the most pages it may
grow to” (64 here). Virtual addresses cost nothing: the region costs memory only for the
pages a program actually touches.
Why a fixed region instead of a per-process “lowest stack page” that moves? With constants,
every question below (“is this address a stack address?”, “which pages might the stack
have?”) has the same answer for every process, with no new field to keep correct across
fork, exec and exit. The price is a fixed maximum. Putting the heap at the top growing
down instead would have changed the meaning of sbrk’s return value for every program.
What it does to p->sz is the real subject of this lab. The stack now lies above
p->sz, so every piece of kernel code that means “all of this process’s memory” when it
says [0, p->sz) is now wrong about the stack. The next three questions find those
places.
The guard is now a page that is simply never mapped, rather than a mapped page with PTE_U
cleared. The old guard used a page of physical memory in every process: on the branch,
right after boot, 2 more pages are free than on the original kernel (32,547 against
32,545; init and sh each saved one).
Check yourself
On the original kernel, a program’s stack needs a second page: the next function call stores below the stack page. What is at that address, and what happens?
With USTACKTOP = TRAPFRAME = 0x3fffffe000, USERSTACK = 64 and one guard page,
what is MAXHEAP, the highest value p->sz may reach? (Answer in hex.)