High-signal notes for coding and debugging the 15-410 kernel.
The kernel supports multiple tasks. A task is a protection domain with its own address space and kernel-owned resources.
A task may contain multiple threads. Threads are the schedulable execution contexts. All threads in a task share the task’s resources and address space.
Memory added to a running task is zeroed before any thread can access it.
IDs are monotonically increasing and thread IDs are small integers, not pointers.
User code traps via INT x using the numbers in spec/syscall_int.h.
One 32-bit argument goes in %esi.
Multi-argument calls pass a memory packet whose address is in %esi.
Return value, if any, is in %eax.
Other registers should stay unchanged unless the syscall spec says otherwise.
Validate every byte of every syscall argument before use.
Check pointers against task VM metadata, not by trusting user values.
Invalid syscall arguments must return a negative error code.
User bugs must not crash, hang, or kill the kernel.
The kernel may not kill a thread except by the spec’s allowed termination paths.
Validity checks may happen early or late if the observable failure still matches the syscall contract.
A thread ends only by vanish(), task_vanish(), or an unhandled exception
in user space.
If the kernel kills a thread, it should print a useful console message and
register dump, then act like vanish().
Killing the last thread in a task should also set the task exit status.
For a killed sole thread, the guide expects the equivalent of
set_status(-2).
vanish() for one thread must not destroy other threads in the same task.
Zombie resources should be reclaimed quickly.
Threads may register one software exception handler with swexn().
Registering a handler is not permanent. The handler is de-registered before it begins running.
If one swexn() call both changes handler registration and loads new
registers, the two requests are all-or-nothing.
Exceptions are synchronous, so the kernel should invoke the handler for the faulting thread if one is registered.
Page faults can be handled by user swexn(), but the kernel may also use
secret page faults internally for VM features like ZFOD or COW.
Only reject eip and esp3 when they are clearly invalid at registration
time. Do not assume future safety.
If a handler is invoked, the saved register state must be sane enough for the handler to run, and undefined registers should be zeroed.
New programs start with an initial stack supplied by exec().
Any later stack growth is user-space responsibility in the Pebbles model.
If the kernel implements automatic stack growth for legacy code, it should do so in the page-fault path, not by changing the spec’s default behavior.
fork() creates a new task with a deep copy of the caller’s address space.
thread_fork creates a new thread in the current task with copied registers
except %eax, and no swexn handler.
exec() replaces the current program after as much validation as possible.
Validate exec() thoroughly before dropping the old address space.
wait() returns the original thread ID of the exiting task, not the last
vanishing thread.
wait() may block, but threads that can never collect a child must not block
forever.
task_vanish(status) makes all task threads vanish in timely fashion.
yield(tid) should mostly affect the caller and the chosen target.
deschedule(reject) and make_runnable(tid) are atomic with respect to each
other.
sleep(ticks) deschedules until at least that many timer interrupts occur.
get_ticks() reports ticks since boot.
new_pages(base, len) requires page-aligned base and positive page-sized
len.
new_pages() must reject overlap with existing mappings or kernel-reserved
regions.
New memory becomes visible to all threads in the task immediately.
remove_pages(base) only removes a region that was created by new_pages()
with the same base.
ZFOD is expected. A shared zero page may be mapped read-only until written.
Be careful with fake mappings. TLB entries can survive page-table edits.
getchar() and readline() serialize access to the input stream.
If another thread is already blocked on input, the new caller waits its turn.
readline() echoes typed characters while a line is active, but characters are
not committed to the buffer until newline.
print() must not intermix output from concurrent callers.
readline() and print() must validate user buffers and reject unreasonable
lengths.
Project 3 guide says getchar() and task_vanish() are not required yet, but
omitted getchar() should be safe and return -1. The user-space exit()
path still depends on a task_vanish-style behavior somewhere.
Boot work should include IDT setup, console clear, frame allocator setup, initial page tables, idle task, init task, then the first context switch.
init is expected to reap orphaned zombies and react sensibly if the shell
exits or dies.
Prefer encapsulation over repeated list traversal. Model sets or queues of threads, not raw linked lists everywhere.
Put traversal links inside the object when a PCB or thread can appear on multiple queues.
Use helper routines for user-memory access and validation instead of ad hoc checks at every call site.
Keep page-fault handlers small and split by region or page semantics.
Design locks up front. Do not plan to add synchronization later.
Interrupt disabling is for short critical sections, not for general locking.
Think in multiprocessor terms even if the target is uniprocessor.
Do not use fixed limits on tasks or threads.
Do not assume invalid user input can be ignored safely.
Do not build giant blob-style allocations for per-item queue nodes if the object can carry its own links.
Do not rely on disabling paging or switching to real mode for normal control flow.
Do not let a scheduler or preemption scheme become a "preemptibility preventer."
Do not let fake mappings leak through sloppy TLB assumptions.
Do not overcomplicate readline() or print() semantics beyond the spec.
Do not turn a page fault into a user-handler event when the kernel intended a secret/internal fault.
Table of Content