Lab 10: Shall
You will work on a worksheet that will prepare you for your shall assignment. Please attend the section you are registered for to receive your lab checkoff.
In the first part of the lab, you will complete two implementation tasks.
- Invoke the
killsystem call without using standard library wrappers. - Implement a signal handler for
SIGSEGVthat recovers from segmentation faults.
In the second part, you will review system calls that will be useful for your upcoming Shall assignment.
Part 1
As you recall from lecture, processes run in an “restricted environment” called the user space. On their own, processes can run computation instructions and access its own memory. However, it cannot directly perform privileged operations such as reading files, creating processes, or writing to files. To request such privileged operations, a process must use system calls, which transfer control to the operating system kernel to perform the requested action safely on its behalf.
In this part of the lab, you will learn how the kill system call works and use it to send signals between processes.
System Calls
Signals are a way for processes to communicate with each other and notify each other of events. For example, when you press Ctrl+C in the terminal, a SIGINT signal is sent to the foreground process to interrupt it. For this exercise, we will write a function called void send_sigint(int pid) that sends a SIGINT signal to the process with the given PID.
Step 1 Learn more about kill
In rv-debug, run the following commands to see how the kill system call is defined by the linux kernel.
cat /usr/include/asm-generic/unistd.h | grep __NR_killcat /usr/include/signal.h| grep kill
You should find that the kill system call is assigned the number 129 and has 2 arguments: process id and signal to send.
Step 2 Write the send_sigint function in RISC-V assembly.
# in send_sigint.s
.global send_sigint
send_sigint:
# the first argument, pid, is already in the correct place (a0)
# the second argument, SIGINT, is 2, and must be stored in the a1 register
addi a1, x0, 2
# load the syscall number for kill (129) into a7
# - write the correct line below
# - for the answer, scroll to bottom of the webpage and reference hint 1
#### BEGIN TODO ####
### END TODO ###
ecall
ret
You can use the assembly above from C code by writing a function declaration for it
// in send_sigint.c
extern void send_sigint(int pid);
int my_atoi(const char *s) {
int n=0, sign=1;
if(*s=='-'||*s=='+') sign=(*s++=='-')?-1:1;
while(*s>='0'&&*s<='9') n=n*10+(*s++-'0');
return sign*n;
}
int main(int argc, char* argv[]) {
if (argc < 2) return 0;
send_sigint(my_atoi(argv[1]));
return 0;
}
Step 3 Compile and run the program.
Ensure you are in the rv-debug docker container. To compile, run
root@1ab8790910e4:~/test# gcc -o send_sigint send_sigint.s send_sigint.c
In a second terminal, run docker exec -it testing /usr/bin/env bash and spawn a new python process
root@1ab8790910e4:~# python3
Python 3.10.12 (main, Sep 11 2024, 15:47:36) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.getpid()
378
If we run qemu ./send_sigint 378, replacing 378 with the PID that you see, we can see that the python process is will print keyboard interrupt. The program you wrote sent a signal to another process without importing any headers!
Exception Handling (OPTIONAL)
An exception is an event that occurs during the execution of a program which disrupts the normal flow of instructions. Examples include division by zero, or references to invalid memory. When an exception occurs, the CPU stops executing the current instruction and transfers control to the operating system. From there, the OS can choose to handle the exception (for example, page faults), or report it to the user program using signals (for example, SEGV for segmentation faults (invalid memory access)).
In this brief example, we will write a program that will handle the SEGV signal. The signal handler will skip over the instruction that caused the exception and continue executing the program!
The following code will abort due to a segmentation fault. Please compile with gcc -O0. Try it out! (don’t forget to use qemu)
volatile int *pointer = 0x0;
int main(void) {
*pointer = 42;
return 0;
}
Now let’s add some code which registers a signal handler for SIGSEGV. The signal handler will modify the program’s context to skip over the instruction that caused the segmentation fault.
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void segv_handler(int sig, siginfo_t *info, void *ctx_void) {
// pointer to context that will be resumed when this context returns
ucontext_t *ctx = (ucontext_t*)ctx_void;
greg_t pc = ctx->uc_mcontext.__gregs[REG_PC];
// NOTE: please don't ever do this in a real program. This is RISC-V specific
// and relies on undefined behavior, which is why we must use "-O0"
// modify the saved program counter so that when we return from this handler,
// execution continues *after* the faulting instruction
ctx->uc_mcontext.__gregs[REG_PC] = pc + 4;
printf("Caught SIGSEGV at PC = 0x%lx (fault address = %p)\n", (long)pc, info->si_addr);
}
volatile int *pointer = 0x0;
int main(void) {
// register the signal handler
struct sigaction sa;
sa.sa_sigaction = segv_handler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, NULL);
// original, problematic code
*pointer = 42;
printf("I'm still alive\n");
return 0;
}
When you run this program, it will catch the segmentation fault, skip over the offending instruction, and print “I’m still alive”.
Hints
addi a7, x0, 129