The pset for this unit concerns our tiny WeensyOS operating system. Our section introduces WeensyOS, and especially its internal interfaces for dealing with virtual memory.
The coding exercises in this section use the cs61-sections/kernels1
subdirectory. This version of WeensyOS uses identity mappings; all processes
share the same page table (the kernel page table), but the kernel is isolated.
This resembles the state of your pset 3 after step 1
(links to the pset page will only work when pset 3 has been released).
Simultaneously enrolled college students are responsible for attending section live (in-person or via zoom). If you cannot attend any section live in a particular week, you may watch a recording (Canvas > Zoom > Cloud Recordings) instead. Please self-report your live attendance (or that you watched a recording) using this attendance form.
Part A: WeensyOS architecture
WeensyOS is a full operating system for x86-64 machines that fits in one directory. In the first part of section, TFs will walk you through the way files are laid out in the operating system and where different operating system functions are located, and show you how to run the operating system in different modes.
Update your cs61-sections
repository and change into the kernels1
subdirectory. Run make run-hello
to see the exciting result of the
p-hello.cc
program!
-
Process code lives in files called
p-*.cc
. Theprocess_main
function is the equivalent ofmain
in C++ programs; when the process starts, its code starts running inprocess_main
. -
Processes can write to the screen using a function called
console_printf
. This function writes to an area of memory called the CGA console, which is a specific region of memory reserved for the screen. (Talk about the address.) -
Discuss the basic layout of WeensyOS memory: kernel code lives at addresses near 0x40000 (4MiB); the kernel stack grows downward from address 0x80000; the process code segment starts at address 0x100000; and the initial process stack segment grows downward from 0x140000.
+-----+--------------------+----------------+--------------------+---------/ | | Kernel Kernel | : I/O | App 1 App 1 | App 2 | | Code + Data Stack | : Memory | Code + Data Stack | Code ... +-----+--------------------+----------------+--------------------+---------/ 0 0x40000 0x80000 0xA0000 0x100000 0x140000 ^ | \___ PROC_SIZE ___/ PROC_START_ADDR
-
Many C library functions are available, but not all. WeensyOS is a standalone operating system: it doesn’t use a shared C library. Check out
lib.hh
andlib.cc
for the available library functions. These are present in both processes and the kernel. -
Before process code runs, the virtual machine that runs WeensyOS emulates a “boot” (short for bootstrapping) process, like the process that runs when a real computer turns on. The first WeensyOS code that runs is in
boot.cc
. This code loads the kernel into memory and then starts running it. -
The kernel is the portion of operating systems code that runs with full machine privilege, meaning that it can control all of memory and other hardware devices. The kernel continues running for as long as the computer has power; its function is to manage processes and hardware. The kernel’s starting point is
kernel_start
inkernel.cc
. This function initializes the hardware and loads the initial process(es). All of the code you will write for the problem set lives inkernel.cc
. -
A powerful thing you can do in kernel code is call the
log_printf
function, which prints information to a file calledlog.txt
.log_printf
is a good debugging tool. -
The other
k-*
C++ files (k-hardware.cc
,k-memviewer.cc
,k-vmiter.cc
,k-vmiter.hh
) provide abstractions that simplify hardware interactions. You’ll deal most deeply withvmiter
, the virtual memory iterator structure; we’ll look into it more deeply in this section. -
A running computer continually switches between process code and kernel code. For instance, the kernel takes control when a process makes a system call by executing a
syscall
instruction; and when the kernel finishes handling the system call, it switches modes back to the process. As part of this protected control transfer into the kernel, the process switches privilege modes, saves some registers, and starts executing kernel code. This mode switch is outside the C++ abstract machine, so it can’t be implemented in C++ alone. The entry sequence is implemented in hand-written assembly ink-exception.S
. Look there if you’re interested! -
Show
make run
,make run-PROCESS
, andmake STOP=1 run
+gdb -ix weensyos.gdb
.
Part B: Virtual memory mappings and permissions
Virtual memory is the processor feature that supports process isolation for primary memory (the part of the computer that stores data and has addresses). It allows different processes to have completely different views of memory. For instance, the data stored at address 0x100000 in the two processes might be totally different, even though the addresses are the same.
Virtual memory is a powerful technique with many uses, though isolation is its most important use now.
x86-64 virtual memory is implemented by page table structures the kernel maintains. A page table maps a virtual address, which is the kind of address used in software and machine instructions, to a physical address, which names a specific set of transistors and capacitors on machine memory chips capable of holding a byte. The virtual-to-physical mapping can be almost arbitrary! All virtual addresses might map to the same set of physical addresses. There is just one constraint: Page tables work in units of aligned chunks of memory 4,096 bytes (212 bytes) big. These aligned units are called pages.
Page tables also associate permissions with each mapping. A mapping can be read-only, which prevents software from writing to memory, and/or kernel-only, which prevents processes from reading or writing memory.
A specific register, the %cr3
register, holds the current page table. If two
processes run with different page tables (different %cr3
values), then they
might have completely different views of memory. This feature lets the kernel
isolate the processes from one another.
Examining mappings and permissions
vmiter
and ptiter
are WeensyOS iterators that process page tables.
vmiter
answers the question “In the context of page table pt
, what does
the virtual address va
map to in terms of physical address and permissions?”
It also lets you modify mappings, thereby changing a process’s view of memory.
ptiter
answers the question “Which physical pages are used to represent this
page table?” This lets you free a page table relatively easily.
EXERCISE B1. Let’s use
vmiter
andlog_printf
to log information aboutkernel_pagetable
’s mappings for the following virtual addresses:
- The
syscall_entry
function;- The
kernel_pagetable
;- The
p-hello
process’s entry point,process_main
.First log the physical addresses mapped for these virtual addresses immediately before the call to
process_setup
inkernel.cc
. What are they?
EXERCISE B2. Now log the physical addresses immediately after the call to
process_setup
. Do the values change? Why or why not? Refer to specific properties ofprocess_setup
.
EXERCISE B3. Log the permissions associated with those virtual addresses immediately before the call to
process_setup
. What are they? What permissions do they correspond to? (Look for thePTE_
constants inx86-64.h
to understand the meaning of each permission bit.)
EXERCISE B4. What about these permissions indicate that the kernel appears to implement kernel isolation (in terms of virtual memory access)?
EXERCISE B5. Log the permissions associated with those virtual addresses immediately after the call to
process_setup
. Did they change?
Part C: Warped virtual memory
Run make run-bigdata
. Boo!
EXERCISE C1. Add one line of virtual memory map manipulation code to
process_startup
so that thebigdata
process printsCS 61 Is Amazing
. Don’t changep-bigdata.cc
or the contents of physical memory, just change memory mappings. If you get stuck, uncomment theconsole_printf
lines inp-bigdata.cc
: what do you see?
Make sure you remove your VM manipulation code before moving on to another problem.
Part D: Using iterators
In the problem set, you will implement fork
and exit
system calls. At the
center of these system calls are operations on virtual memory that you’ll use
vmiter
and ptiter
to implement. In section we investigate related problems
for practice.
EXERCISE D1. Use
vmiter
to implement the following function.void copy_mappings(x86_64_pagetable* dst, x86_64_pagetable* src) { // Copy all virtual memory mappings from `src` into `dst` // for addresses in the range [0, MEMSIZE_VIRTUAL). // You may assume that `dst` starts out empty (has no mappings), // and that all calls to `vmiter::map` succeed. // After this function completes, for any va with // 0 <= va < MEMSIZE_VIRTUAL, dst and src should map that va // to the same pa with the same permissions. ... }
The fork
system call, which you’ll implement in the pset, is the original
system call that Unix-based operating systems used to start new processes.
fork
works by creating a new process, called the child process, that is
essentially a copy of the parent process (the process that called fork
).
The child process has a copy of the parent process’s memory, as well as its
registers and other state. After the system call completes, then, there are
two processes whose contents of memory are identical. The memory contents
quickly diverge, though: changes to the child process’s memory are not visible
in the parent’s memory or vice versa.
EXERCISE D2. Does
copy_mappings
suffice to implement the memory copying required byfork
? Why or why not?
EXERCISE D3. Use
vmiter
andptiter
to implement the following function.void free_everything(x86_64_pagetable* pt) { // Free the following pages by passing their kernel pointers // to `kfree(void*)`: // 1. All memory accessible via unprivileged mappings in `pt` from virtual // addresses in [0,MEMSIZE_VIRTUAL). // 2. All page table pages that are part of `pt`. ... }
The exit
system call allows a process to stop executing. All memory
belonging to or representing that process must be returned to the kernel for
reuse. This will include some memory that is accessible only to the kernel,
such as memory storing the process’s page table.
EXERCISE D4. Does
free_everything
suffice to implement the memory freeing required byexit
? Why or why not?
Offline Part E: Spawn
This and the following parts won’t be covered in section by default. They are optional practice to prepare for the problem set or exams. We suggest groups of students get together and work through them on their own.
In the first part, we start a new process in WeensyOS. Starting a new process
requires a couple things: choosing a struct proc
member of the ptable
array, initializing memory, initializing registers, and marking the process as
runnable. The fork
system call in pset 3 step 5
starts a process by copying an already-running process, but other interfaces
are possible. The spawn
interface, for example, starts a new process from
scratch.
EXERCISE E1. The
p-spawn.cc
process tries to start another process—a copy of thep-alice
program—by callingsys_spawn("alice")
. Usemake run-spawn
(ormake run-console-spawn
, etc.) to run this program.
- The system call is not working. How can you tell?
- Complete the system call implementation so that
p-spawn.cc
successfully starts a new process.- Modify
p-spawn.cc
so that it tries to start two copies ofalice
. What happens and why? How could you fix this? Refer to the steps of pset 3.- Does your implementation of
syscall_spawn
check its arguments for validity? If not, how would you change it to do this?
Offline Part F: Confused deputy
A confused deputy attack occurs when a low-privilege attacker convinces a privileged “deputy” to complete an attack on its behalf. In the context of operating systems, a process is unprivileged, while the kernel has full privilege. However, a system call asks the kernel to perform an operation on behalf of a process, making the kernel act as a privileged deputy. A confused deputy attack occurs if the process, by invoking system calls, can somehow convince the kernel to perform a function that violates process isolation.
EXERCISE F1.
p-eve.cc
andp-alice.cc
may be familiar from class. Usemake run-friends
to run Alice and Eve together.
- It is possible to change one argument of one system call in
p-eve.cc
to execute a successful confused deputy attack that breaks the kernel or otherwise prevents Alice from running. What is the system call? What is the confused deputy attack?- Complete the attack.
- Modify the kernel’s system call checking to prevent the attack. The kernel might kill the Eve process upon detecting the attack, or (better perhaps) return -1 from the relevant system call.
Offline Part G: Efficient communication
The central theme of Unit 3 is isolation and security, but kernels also can offer design features that transform overall system performance.
The p-pipewriter.cc
and p-pipereader.cc
programs implement a simple form
of communication, mediated by the kernel. The pipewriter program picks a
random message and writes it to a shared “pipe,” which is just a memory buffer
located in the kernel. The pipereader program reads this message by reading
from the pipe.
EXERCISE G1. Use
make run-pipe
to run the pipewriter and pipereader together.
- Read the explanations for the
sys_piperead
andsys_pipewrite
system calls inu-lib.hh
. What do these system calls remind you of? How do their parameters differ from the parameters to analogous functions in Unix/Linux?- Find the implementations of
SYSCALL_PIPEWRITE
andSYSCALL_PIPEREAD
inkernel.cc
. What is the maximum number of bytes these system calls can read or write at a time? How might this be a performance problem?- How many system calls do the writer and reader make per message? Is there a discrepancy? Look at their code; can you explain the discrepancy?
EXERCISE G2. Change the kernel, and only the kernel, to reduce the number of system calls required to transfer a message to the minimum, which is less than five. Here are some techniques you can use:
- Scheduling. When process P is unlikely to be doing useful work, it often makes sense to run a process other than P. The handout kernel does not follow this rule of thumb.
- Batching/buffering. It is more efficient to transfer more than one byte at a time.
- Blocking. When process P cannot make progress until some state changes, it can be more efficient overall to put process P to sleep. This is called blocking process P. For instance, perhaps the
sys_piperead
system call might block the calling process until there is at least one byte available to read.