System calls are interfaces that allow user applications to request various services from the operating system kernel. This project aims to explore and understand how system calls are implemented in the xv6
operating system.
In RISC-V, a trap is a general term that encompasses both exceptions and interrupts (see Chap. 4 of the xv6 book). Exceptions are typically generated by the CPU itself in response to events such as illegal instructions, memory access faults, system call invocations via the ecall
instruction, etc. Interrupts, by contrast, are triggered by external signals from devices or timers that indicate they require immediate attention.
RISC-V supports multiple privilege levels (machine, supervisor, and user modes), each with its own set of capabilities and restrictions. Traps can be configured to be handled at different privilege levels depending on their type and the processor's current operating mode. When a trap occurs, the RISC-V hart automatically fills a register (mcause
or scause
depending on the privilege level) with a value that indicates the reason for the trap, as shown in the following table.
Interrupt | Exception code | Description |
---|---|---|
0 | 0 | Instruction address misaligned |
0 | 1 | Instruction access fault |
0 | 2 | Illegal instruction |
0 | 3 | Breakpoint |
0 | 4 | Load address misaligned |
0 | 5 | Load access fault |
0 | 6 | Store/AMO address misaligned |
0 | 7 | Store/AMO access fault |
0 | 8 | Environment call from U-mode |
0 | 9 | Environment call from S-mode |
0 | 10 | Reserved |
0 | 11 | Environment call from M-mode (mcause only) |
0 | 12 | Instruction page fault |
0 | 13 | Load page fault |
0 | 14 | Reserved |
0 | 15 | Store/AMO page fault |
0 | >= 16 | Reserved |
1 | 0 | Reserved |
1 | 1 | Supervisor software interrupt |
1 | 2 | Reserved |
1 | 3 | Machine software interrupt (mcause only) |
1 | 4 | Reserved |
1 | 5 | Supervisor timer interrupt |
1 | 6 | Reserved |
1 | 7 | Machine timer interrupt (mcause only) |
1 | 8 | Reserved |
1 | 9 | Supervisor external interrupt |
1 | 10 | Reserved |
1 | 11 | Machine external interrupt (mcause only) |
1 | >= 12 | Reserved |
Besides the mcause
(or scause
) register, various additional registers are used to handle traps; when a trap is taken into M-mode (or S-mode), mepc
(or sepc
) register is written with the virtual address of the instruction that was interrupted or that encountered the exception. The mtvec
(or stvec
) register holds the start address of the trap handler in M-mode (or S-mode). Also, the mstatus
(or sstatus
) register keeps track of important information such as M-mode or S-mode interrupt-enable bits (MIE
or SIE
bit), the value of the interrupt-enable bit active prior to the trap (MPIE
or SPIE
bit), and the previous privilege mode (MPP
or SPP
bits).
To increase performance, certain exceptions and interrupts can be handled at a lower privilege level. For example, setting a bit in medeleg
or mideleg
register will delegate the corresponding trap or interrupt, when occurring in S-mode or U-mode, to the S-mode trap handler. By default, xv6
delegates all interrupts and exceptions to S-mode.
Each system call in xv6
is assigned a unique number, as you can see in kernel/syscall.h
. This number is used to identify which system call is being requested by a user program. When a user program makes a system call, it places the system call number in a designated register, a7
, and the arguments for the system call in other registers from a0
to a6
.
The user program then executes a special ecall
instruction, which triggers a trap from U-mode to S-mode, transferring the control to the trap handler uservec() @ kernel/trampoline.S
. After saving user register contexts and switching into the kernel address space, the RISC-V hart jumps into usertrap() @ kernel/trap.c
. Note that both system calls and interrupts originating from U-mode are directed to usertrap()
. If the sret
(return-from-S-mode) instruction is executed at the end of the system call handler, the control returns to U-mode.
Additionally, to ensure the program resumes execution at the instruction following the ecall
instruction, the value of sepc
should be explicitly incremented by 4 before executing the sret
instruction.
The same mechanism can be used for the kernel running in S-mode to request services from M-mode. Specifically, when the kernel executes the ecall
instruction in S-mode, control is transferred to the M-mode trap handler (unless it is delegated to S-mode). If the hart executes mret
(return-from-M-mode) instruction in M-mode, control is returned to the location specified by the mepc
register.
Physical Memory Protection (PMP) in RISC-V is a hardware feature that provides fine-grained control over access to memory regions. It allows the system to define a set of rules governing which memory regions can be accessed by different privilege levels (such as U-mode or S-mode). RISC-V supports up to 64 PMP entries, with each PMP entry defined by an 8-bit configuration register (e.g., pmp0cfg
) and a corresponding 64-bit address register (e.g., pmpaddr0
).
The PMP address registers are named pmpaddr0
through pmpaddr63
. Each PMP address register encodes bits 55-2 of a 56-bit physical address. The PMP configuration registers, ranging from pmp0cfg
to pmp63cfg
, are densely packed into 64-bit registers, pmpcfg0
to pmpcfg14
, to minimize context-switch time. For example, pmpcfg0
contains eight configuration registers from pmp0cfg
to pmp7cfg
. Each bit in the PMP configuration register specifies whether the corresponding memory region has permission for read (R
), write (W
), or instruction execution (X
), as illustrated in the following figure. The A
field encodes the address-matching mode of the associated PMP address register. In this project assignment, we will consider only the case of A
= 0b01
, which indicates that the associated PMP address register defines the top of the address range (TOR), with the preceding PMP address range forming the bottom of the range.
The L
bit indicates that the PMP entry is locked, i.e., writes to the configuration register and associated address registers are ignored. You can assume that the L
bit is set to 0 (unlocked). Note that the PMP address registers and configuration registers are accessible only in M-mode.
PMP configuration register format:
7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| L | 0 | 0 | A | X | W | R |
+---+---+---+---+---+---+---+---+
In xv6
, the PMP registers are initialized in the start()
function at ./kernel/start.c
as follows. This gives the read, write, and execute permissions to the entire area of physical memory. (In the skeleton code, the corresponding initialization is done in setpmp()
@ ./kernel/pmp.c
.)
void
start()
{
...
// configure Physical Memory Protection to give supervisor mode
// access to all of physical memory.
w_pmpaddr0(0x3fffffffffffffull);
w_pmpcfg0(0xf);
...
}
First, you need to implement the nenter()
system call. The system call number of nenter()
is already assigned to 22 in the ./kernel/syscall.h
file.
SYNOPSYS
int nenter();
DESCRIPTION
The nenter()
system call returns the total count of [ENTER]
key presses from the console input device (i.e., keyboard) since the system booted.
RETURN VALUE
nenter()
returns the cumulative count of[ENTER]
key presses from the keyboard. This count is initialized to zero at kernel startup and is expected to increase monotonically thereafter.
You are required to implement the getpmpaddr()
system call. The system call number of getpmpaddr()
is already assigned to 23 in the ./kernel/syscall.h
file.
SYNOPSYS
void *getpmpaddr(int n);
DESCRIPTION
The getpmpaddr()
system call returns the 64-bit physical address stored in the PMP address register. The value n
denotes the index of the PMP address register (e.g., 0 for pmpaddr0
, 1 for pmpaddr1
, etc.). You can assume that n
is an integer value between 0 and 3. The contents of the PMP address registers can be read using the following RISC-V assembly instruction.
# Reading pmpaddr0 register
csrr a0, pmpaddr0
The PMP address registers contain only bits 55-2 of the physical address, so after reading the value using the csrr
instruction, you need to shift the result left by 2 bits to obtain the full address. The remaining bits 63-56 are set to zero. Since PMP address registers are only accessible in machine mode, another (nested) system call from supervisor mode to machine mode is required to retrieve their values.
RETURN VALUE
getpmpaddr()
returns a 64-bit physical address, where bits 55-2 are obtained from then
-th PMP address register (e.g.,pmpaddr0
,pmpaddr1
, etc.), and the remaining bits are set to zero.- If
n
is less than 0 or greater than 3,getpmpaddr()
returns(void *) -1
.
Finally, you need to implement the getpmpcfg()
system call. The system call number of getpmpcfg()
is already assigned to 24 in the ./kernel/syscall.h
file.
SYNOPSYS
int getpmpcfg(int n);
DESCRIPTION
The getpmpcfg()
system call returns an integer, where the lower 8 bits represent the content of the specified PMP configuration register. The value n
denotes the index of the PMP configuration register (e.g., 0 for pmp0cfg
, 1 for pmp1cfg
, etc.). You can assume that n
is an integer value between 0 and 3. Since individual 8-bit PMP configuration registers, such as pmp0cfg
and pmp1cfg
, cannot be read directly, you must read the entire 64-bit pmpcfg0
register, which contains pmp0cfg
through pmp7cfg
, and then extract the corresponding 8-bit entry. The 64-bit PMP configuration registers can be accessed using the following RISC-V assembly instruction.
# Reading pmpcfg0 register
csrr a0, pmpcfg0
Similar to PMP address registers, PMP configuration registers are only accessible in machine mode. Therefore, you must make another (nested) system call from supervisor mode to machine mode to retrieve their values.
RETURN VALUE
getpmpcfg()
returns a 32-bit integer, where lower 8 bits (bits 7-0) are obtained from then
-th PMP configuration register (e.g.,pmp0cfg
,pmp1cfg
, etc.), and the remaining bits are set to zero.- If
n
is less than 0 or greater than 3,getpmpcfg()
returns-1
.
- For this project assignment, you should use the
qemu
version 8.2.0 or higher. To determine theqemu
version, use the command:$ qemu-system-riscv64 --version
- You only need to change the files in the
./kernel
directory. Any other changes will be ignored during grading. - Do not change the
./kernel/pmp.c
file. This file will be overwritten during the automatic grading process.
-
Read Chap. 4.1 of the xv6 book to understand RISC-V's privileged modes (supervisor mode and machine mode) and trap handling mechanism.
-
Read Chap. 4.2 ~ 4.5 of the xv6 book to see how traps (system calls and interrupts) are handled in xv6.
-
Read Chap. 5 of the xv6 book to learn about hardware interrupts.
-
More detailed information on physical memory protection (PMP) can be found in Chap. 3.7 of the RISC-V Privileged Architecture manual.
-
For your reference, the following roughly shows the amount of changes you need to make for this project assignment. Each
+
symbol indicates 1~10 lines of code that should be added, deleted, or altered.kernel/console.c | + kernel/kernelvec.S | ++++ kernel/riscv.h | + kernel/start.c | + kernel/syscall.c | + kernel/sysproc.c | ++++++ kernel/trap.c | +
The skeleton code for this project assignment (PA2) is available as a branch named pa2
. Therefore, you should work on the pa2
branch as follows:
$ git clone https://github.com/snu-csl/xv6-riscv-snu
$ git checkout pa2
After downloading, you must first set your STUDENTID
in the Makefile
again.
The pa2
branch has a user-level utility program called nenter
whose source code is available in the user/nenter.c
file. The nenter
program simply calls the nenter()
system call and prints its result. If you successfully implement the nenter()
system call, the output should look like this:
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
xv6 kernel is booting
hart 2 starting
hart 1 starting
init: starting sh
$ nenter
nenter: 1
$ nenter
nenter: 2
$ nenter
nenter: 3
$ QEMU: Terminated
There is also a user-level program called pmptest
in the user
directory. Its source code can be found in the user/pmptest.c
file. This program displays the values of PMP address registers from pmpaddr0
to pmpaddr3
, along with their corresponding permissions, after reading them using the getpmpaddr()
and getpmpcfg()
system calls. The following shows what you would get under the xv6
's default PMP setting.
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
xv6 kernel is booting
hart 2 starting
hart 1 starting
init: starting sh
$ pmptest
pmpaddr[0] = 0x00FFFFFFFFFFFFFC (RWX)
pmpaddr[1] = 0x0000000000000000 (---)
pmpaddr[2] = 0x0000000000000000 (---)
pmpaddr[3] = 0x0000000000000000 (---)
$ QEMU: Terminated
-
First, make sure you are on the
pa2
branch in yourxv6-riscv-snu
directory. And then perform themake submit
command to generate a compressed tar file namedxv6-{PANUM}-{STUDENTID}.tar.gz
in the../xv6-riscv-snu
directory. Upload this file to the submission server. You don't need to upload any documents for this project assignment. -
The total number of submissions for this project assignment will be limited to 30. Only the version marked as
FINAL
will be considered for the project score. Please remember to designate the version you wish to submit using theFINAL
button. -
Note that the submission server is only accessible inside the SNU campus network. If you want off-campus access (from home, cafe, etc.), you can add your IP address by submitting a Google Form whose URL is available in the eTL. Now, adding your new IP address is automated by a script that periodically checks the Google Form at minutes 0, 20, and 40 during the hours between 09:00 and 00:40 the following day, and at minute 0 every hour between 01:00 and 09:00.
- If you cannot reach the server a minute after the update time, check your IP address, as you might have sent the wrong IP address.
- If you still cannot access the server after a while, it is likely due to an error in the automated process. The TAs will check if the script is running properly, but since that is a manual process, please do not expect it to be completed immediately.
- You will work on this project alone.
- Only the upload submitted before the deadline will receive the full credit. 25% of the credit will be deducted for every single day delayed.
- You can use up to 3 slip days during this semester. If your submission is delayed by one day and you decide to use one slip day, there will be no penalty. In this case, you should explicitly declare the number of slip days you want to use on the QnA board of the submission server before the next project assignment is announced. Once slip days have been used, they cannot be canceled later, so saving them for later projects is highly recommended!
- Any attempt to copy others' work will result in a heavy penalty (for both the copier and the originator). Don't take a risk.
Have fun!
Jin-Soo Kim
Systems Software and Architecture Laboratory
Dept. of Computer Science and Engineering
Seoul National University