# Ćwiczenia 2, grupa cz. 10-12, 20 października 2022
###### tags: `SO22` `ćwiczenia` `pwit`
## Deklaracje
Gotowość rozwiązania zadania należy wyrazić poprzez postawienie X w odpowiedniej kolumnie! Jeśli pożądasz zreferować dane zadanie (co najwyżej jedno!) w trakcie dyskusji oznacz je znakiem ==X== na żółtym tle.
**UWAGA: Tabelkę wolno edytować tylko wtedy, gdy jest na zielonym tle!**
:::danger
| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| ----------------------:| ----- | --- | --- | --- | --- | --- | --- | --- |
Miriam Bouhajeb | X | | X | X | X | | | |
Kacper Chmielewski | X | X | X | X |==X==| X | | |
Jan Jankowicz | | | | | | | | |
Jakub Kaczmarek | | | | | | | | |
Jarosław Kadecki | X | | X | X | X | X | | |
Zuzanna Kania | X | | X | | X | | | |
Julia Konefał | | | X | X | | | | |
Daniel Sarniak | X | X | X | X | | X | X | X |
Paweł Tkocz | X | | X | X | | | | |
Miłosz Urbanik | X | | X | X | X | X | | X |
Tomasz Wołczański | X | | X | X | X | X | X | X |
Radosław Śliwiński | X | | X | X | | | | |
:::
:::info
**Uwaga:** Po rozwiązaniu zadania należy zmienić kolor nagłówka na zielony.
:::
## Zadanie 1
:::success
Autor: Miłosz Urbanik

:::

Stany
1. Stopped - zatrzymany sygnałem `SIGSTOP` lub `SIGTSTP`. Proces nie wykonuje się, ani nie odbiera sygnałów(z wyjątkiem `SIGKILL`) do momentu otrzymania sygnału `SIGCONT`
2. Running
2.1. Ready - proces załadowany do pamięci, czeka na przydzielenie czasu CPU przez scheduler
2.2 Executing - proces wykonywany przez CPU
3. Zombie - proces zakończony, ale nie pogrzebany
4. Sleep
4.1 Uninterruptible - proces jest zablokowany i nie reaguje na sygnały, tylko na dostęp do zasobów sprzętowych. Czeka do momentu zakończenia wywołania systemowego - najczęściej dostępu do dysku np. `mkdir`
4.2 Interruptible - proces zablokowany podobnie jak uninterruptible, ale odbiera sygnały
* ignorowanie sygnału - ustawienie `sigaction` na `SIG_IGN`, po odebraniu sygnału proces wraca do wykonywanej instrukcji, nie obsługuje sygnału.
* blokowanie sygnału - dodanie sygnału do maski blokowanych sygnałów.
Jądro nie przekazuje sygnału do procesu, zaznacza tylko sygnał jako `pending` w masce oczekujących sygnałów. Sygnał jest przekazywany do procesu dopiero po odblokowaniu odpowiedniego sygnału.
Za manualem:
`The signals SIGKILL and SIGSTOP cannot be caught or ignored.`
Zablokowanie SIGKILL zostanie "po cichu" zignorowane.
SIGSEGV można zarówno blokować, jak i ignorować.
`According to POSIX, the behavior of a process is undefined after it ignores a SIGFPE, SIGILL, or SIGSEGV signal that was not generated by kill(2) or raise(3)`
Jeśli zignorujemy SIGSEGV w programie z błędnym dostępem do pamięci, to po "zignorowaniu" sygnału wracamy do instrukcji, która ponownie spowoduje SIGSEGV.
```c=
#include "include/csapp.h"
#include <bits/types/sig_atomic_t.h>
int main(){
printf("my pid is %d\n", getpid());
signal(SIGSEGV, SIG_IGN);
printf("SIGSEGV ignored\n");
sleep(20); //tutaj kill -SIGSEGV [pid] nie powoduje zatrzymania procesu
printf("sleep finished\n");
signal(SIGSEGV, SIG_DFL);
sigset_t block_segv, waiting_mask;
sigemptyset(&block_segv);
sigaddset(&block_segv, SIGSEGV);
sigprocmask(SIG_SETMASK, &block_segv, NULL);
printf("SIGSEGV blocked\n");
sleep(20); // kill -SIGSEGV [pid] nie zatrzymuje procesu
sigpending(&waiting_mask); //sigpending sprawdza sygnały oczekujące
if(sigismember(&waiting_mask, SIGSEGV)) //jeśli wysłaliśmy sygnał to jest to członkiem maski pending
printf("SIGSEGV pending\n");
else
printf("SIGSEGV lost\n");
}
```
## Zadanie 2
:::success
Autor: Daniel Sarniak

**Tworzenie procesów w systemie:**
**Linux:**
* Zostaje utworzony nowy deskryptor procesu (`task_struct` - struktura przechowująca wszystkie informacje o danym procesie) i obszar użytkownia dla procesu potomnego. Większość danych zostaje przepisana od rodzica.
* Dziecko dostaje swój identyfikator PID
* Proces potomny otrzymuje mapę pamięci i współdzielony dostęp do plików swojego rodzica
* Rejestry zostają usawione i proces staje się gotowy do wykonania.
**WinNT:**
Tutaj procesy tworzy się w inny sposób niż w Linuxie. Zamiast wywołania `fork()` używane jest wywołanie `CreateProcess`, tworzące zupełnie nowy proces, a nie klon procesu jak w Linuxie. `CreateProcess` wykonuje program podany mu jako argument. Stworzony proces nie otrzymuje kopii pamięci swojego rodzica `CreateProcess` można w pewien sposób porównać do połączenia `fork` z `execve`.
Grafika z podanej literatury przedstawiająca przebieg tworzenia nowego procesu przy pomocy `fork()` + `exec()`

* Kiedy `fork` zostaje wywołany, kernel tworzy `task_struct` i struktury danych takie jak `kernel-mode stack` czy `thread_info`. Nowa struktrua zadania, umieszczana jest w pamięci na pozycji przesuniętej o daną stałą wartość względem końca stosu danego procesu, reprezentuje szereg parametrów procesu, w tym adres jego deskryptora. Znaczna większość zawartości deksryptora jest kopiowana z procesu rodzica. Następnie szuka się wolnego identyfikatora PID i uaktualnia tablicę haszującą tak, żeby PID wskazywało na naszego nowego `task_structa`. Na koniec kopiuje się pamięć rodzica. Choć nie jest to do końca trafne stwierdznie. Pamięć powina być kopiowana z definicji `fork`, lecz jest to bardzo kosztowna operacja. Nowoczesne Linuxy stosują więc pewne oszustwo. Dają procesowi potomnemu własną tablice stron, która wskazuje na strony rodzica (ale w trybie read-only). Jeżeli dowolny z procesów (dziecko/rodzic) będzie chicało zapisać na tej stronie otrzyma `protection foult`. Kernel widząć to alokuje nową kopie strony do tego procesu jako read/write, dzięki czemu tylko strony faktycznie używane do zapisu będą kopiowane. Mechanizm ten nazywa się copy on write.
* Wywołanie `exec` sprawia, że jądro zaczyna od szukania programu wykonywalnego, następnie sprawdza uprawnienia wykonywania, po czym kopiuje argumenty i środowisko oraz zwalnia starą przestrzeń adresową, i tablicę stron. Tworzona zostaje nowa przestrzeń adresowa ale nie zostaje ona uzupełniona. Start procesu skutkuje `page fault`, który sprawia, że pierwsza strona kodu zostaje wprowadzona z pliku wykonywalnego. Dzięki temu, nie musimy ładować danych z wyprzedzeniem, przez co programy mogą uruchamiać się szybko. Na koniec argumenty i zmienne środowiskowe zostają skopiowane na stos, sygnały zostają zresetowane a rejestry wypełnione zerami.

Jak widać, wywołanie naraz połączonego wywołania `fork()`+`execve()` jako `spawn()` pozbawia nas mozliwości wprowadzania zmian pomiędzy wywołaniami `fork` i `execve` co powoduje problemy np z potokami.
Najłatwiej wytłumaczyć to właśnie na podstawie **potków**. Jeżeli chicelibyśmy użyć potoku pomiędzy dwiema komendami potrzebujemy dwóch deskryptorów plików, które chcemy odpowiednio przekierowywać na `stdin` i `stdout` potoku. Przykładowo, przed wykonaniem `execve()` musimy ustawić jego `stdin` na "reading end of pipe".
Przykład implementacji `ls | less` znaleziony w internecie
http://www.cse.cuhk.edu.hk/~ericlo/teaching/os/lab/6-IPC1/pipe-shell.html
```c
/* pipe4.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int main(int agrc, char* agrv[])
{
int pipefds[2];
pid_t pid;
if(pipe(pipefds) == -1){
perror("pipe");
exit(EXIT_FAILURE);
}
pid = fork();
if(pid == -1){
perror("fork");
exit(EXIT_FAILURE);
}
if(pid == 0){
//replace stdout with the write end of the pipe
dup2(pipefds[1],STDOUT_FILENO);
//close read to pipe, in child
close(pipefds[0]);
execlp("ls","ls",NULL);
exit(EXIT_SUCCESS);
}else{
//Replace stdin with the read end of the pipe
dup2(pipefds[0],STDIN_FILENO);
//close write to pipe, in parent
close(pipefds[1]);
execlp("less","less",NULL);
exit(EXIT_SUCCESS);
}
}
```
Przykład potoków: często wykorzystywany na poprzedniej liście:
`ps ax | grep X`
**Przekierowanie** przekierowuje `stdout` procesu na początek pliku
:::
## Zadanie 3
:::success
Autor: Julia Konefał
:::
:::info
Zadanie 3. Na podstawie dokumentacji fork(2) (§8.3) i execve(2) (§8.10) wymień najważniejsze zasoby procesu, które są (a) dziedziczone przez proces potomny (b) przekazywane do nowego programu załadowanego do przestrzeni adresowej. Czemu przed wywołaniem fork należy opróżnić bufory biblioteki stdio(3)? Co jądro robi w trakcie wywołania execve z konfiguracją zainstalowanych procedur obsługi sygnałów?
:::
a) fork (tworzy proces potomny) przekazuje:
* identyfikatory: real user/group ID, effective user/group ID, process group ID
* ustawienia środowiska:
USER, LOGNAME (nazwa użytkownika),
HOME (katalog domowy)
LANG (locale)
PATH (prefiksy nazw plików, które programy przeszukują, kiedy szukają plików o niekompletnej nazwie)
PWD (aktualna ścieżka)
TERM (typ terminala, dla którego przygotowywany jest output)
* aktualną ścieżkę, w której się znajduje
* maska uprawnień pliku
* terminal, który je kontroluje
* signal handlers
* dzielona pamięć
* mapy pamięci
* limit zasobów
Przed jego wywołaniem należy opróżnić bufory stdio, ponieważ chcemy uniknąć kilkukrotnego wypisywania danych.
b) execve (wywołuje nowy program) przekazuje:
* identyfikatory: process -/parent/group ID, real user/group ID, supplementary groups IDs
* deskryptory plików
* dzielona pamięć
* czas pozostały do alarm clock signal
* aktualną ścieżkę
* maskę sygnałów (zablokowane sygnały)
* oczekujące sygnały
* czasy egzekucji procesów
* terminal, który go kontroluje
**Zainstalowane** procedury obsugi sygnałów – procedury definiujące obsługę sygnałów SIGUSR1 i SIGUSR2.
Jądro przywraca standardową obsługę tylko tych sygnałów, dla których użytkownik sam ustawił wcześniej nową procedurę obsługi. Oznacza to, ze sygnały, które były ignorowane w programie wywołującym, pozostaną takimi w programie wywoływanym.
## Zadanie 4
:::success
Autor: Jarosław Kadecki
:::

Domyslnie wysłane sygnały:
kill - służy do wysyłania sygnału do procesu sprecyzowanego PID'em. Domyślnie TERM
pkill - służy do wysyłania sygnału do procesu sprecyzowanego nazwą. Domyślnie SIGTERM
xkill - służy do zamykania polączenia procesu z serwerem X.
Sygnały oczekujące - sygnały których dostarczenie jest wstrzymane do momentu wyjścia z nieprzerywalnego snu
SigQ to pole zawierające dwie liczby które oznaczają liczbę zakolejkowanych sygnałów dla real user ID oraz limit zakolejkowanych sygnałów dla tego procesu.
SigPnd, ShdPnd to pola z liczbą sygnałów oczekujących na wątek oraz na cały proces.
SigBlk, SigIgn, SigCgt to maski sygnałów blokowanych, ignorowanych oraz złapanych.
Sygnały oczekujące - sygnały których dostarczenie jest wstrzymane do momentu wyjścia z nieprzerywalnego snu
Wysyłane sygnały:
SIGUSR1 - 10
SIGUSR2 - 12
SIGHUP - 1 - Hangup detected on controlling terminal
or death of controlling process
SIGINIT - 2
nasłuchujemy jakie sygnały dochodzą do xeyes przy użyciu:
strace -e trace=signal -p PID
Zatrzymujemy xeyes
wysyłamy sygnały:
kill -10 PID
kill -12 PID
kill -1 PID
kill -2 PID
Wznawiamy proces
kill -18 PID
Sprawdzamy na strac'e, że SIGHUP jest pierwszym obsługiwanym sygnałem.
## Zadanie 5
:::success
Autor: Kacper Chmielewski

Jakie zadanie pełni minimalny program rozruchowy?
- wchodzi do roota,
- przygotowywuje tablice sygnałów tak aby wszystkie sygnał← z POSIX.1-2017 były zawarte,
- wywołuje funkcje init,
- po czym czuwa czy są wysyłane sygnały zawarte w sigmap.
Możemy więc zauważyć, że ten program po wywołaniu 'init', co jakiś czas sprawdza czy należy rozpatrzeć jakiś sygnał z tablicy sigmap.
Jakie akcje wykonuje pod wpływem wysyłania do niego sygnałów wymienionych w tablicy «sigmap»?
Wywołuje funkcję (która jest przechowywana pod wskaźnikiem w sigmapie), obsługi danego sygnału.
Do czego służą procedury sigprocmask(2) i sigwait(3)?
Sigprocmask rozpatruje i zmnienia blockowane sygnały. / jest używany do dodania lub zmieniea sygnały maski dla wywołującego ją wątku. Maska sygnału jest zbiorem sygnałów które dostarczenie jest obecnie zblokowane przez callera.
Sigwait jest funckją, która zawiesza wykonanie wywołanego wątku, aż dostanie sygnał "set".
W jaki sposób grzebie swoje dzieci?
Zaa pomocą funckji sigreap, w której jest wywoływana funkcja waitpid.
```=C
/* See LICENSE file for copyright and license details. */
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define LEN(x) (sizeof (x) / sizeof *(x))
#define TIMEO 30
static void sigpoweroff(void);
static void sigreap(void);
static void sigreboot(void);
static void spawn(char *const []);
static struct {
int sig;
void (*handler)(void);
} sigmap[] = {
{ SIGUSR1, sigpoweroff },
{ SIGCHLD, sigreap },
{ SIGALRM, sigreap },
{ SIGINT, sigreboot },
};
#include "config.h"
static sigset_t set;
int main(void){
int sig;
size_t i;
if (getpid() != 1)
return 1;
chdir("/");
sigfillset(&set);
sigprocmask(SIG_BLOCK, &set, NULL);
spawn(rcinitcmd);
while (1) {
alarm(TIMEO);
sigwait(&set, &sig);
for (i = 0; i < LEN(sigmap); i++) {
if (sigmap[i].sig == sig) {
sigmap[i].handler();
break;
}
}
}
/* not reachable */
return 0;
}
static void sigpoweroff(void){
spawn(rcpoweroffcmd);
}
static void sigreap(void) {
while (waitpid(-1, NULL, WNOHANG) > 0);
alarm(TIMEO);
}
static void sigreboot(void){
spawn(rcrebootcmd);
}
static void spawn(char *const argv[]){
switch (fork()) {
case 0:
sigprocmask(SIG_UNBLOCK, &set, NULL);
setsid();
execvp(argv[0], argv);
perror("execvp");
_exit(1);
case -1:
perror("fork");
}
}
```
:::
## Zadanie 6
:::success
Autor: Tomasz Wołczański
:::

```c
#include "csapp.h"
static pid_t spawn(void (*fn)(void)) {
pid_t pid = Fork();
if (pid == 0) {
fn();
printf("(%d) I'm done!\n", getpid());
exit(EXIT_SUCCESS);
}
return pid;
}
static void grandchild(void) {
printf("(%d) Waiting for signal!\n", getpid());
pause();
printf("(%d) Got the signal!\n", getpid());
}
static void child(void) {
pid_t pid;
setpgrp();
pid = spawn(grandchild);
printf("(%d) Grandchild (%d) spawned!\n", getpid(), pid);
}
/* Runs command "ps -o pid,ppid,pgrp,stat,cmd" using execve(2). */
static void ps(void) {
char *argv[] = {"ps", "-o", "pid,ppid,pgrp,stat,cmd", NULL};
execvp(argv[0], argv);
}
int main(void) {
/* TODO: Make yourself a reaper. */
#ifdef LINUX
Prctl(PR_SET_CHILD_SUBREAPER, 1);
#endif
printf("(%d) I'm a reaper now!\n", getpid());
pid_t pid, pgrp;
int status;
pid = spawn(child);
pgrp = pid;
printf("(%d) Child (%d) spawned!\n", getpid(), pid);
waitpid(pid, &status, 0);
printf("(%d) Child (%d) terminated with status %d\n", getpid(), pid, status);
pid = spawn(ps);
printf("(%d) ps (%d) spawned!\n", getpid(), pid);
waitpid(pid, &status, 0);
printf("(%d) ps (%d) terminated with status %d\n", getpid(), pid, status);
kill(-pgrp, SIGINT);
waitpid(-pgrp, &status, 0);
printf("(%d) Grandchild (%d) killed - terminated with status %d\n", getpid(), pid, status);
return EXIT_SUCCESS;
}
```

## Zadanie 7
:::success
Autor: Tomasz Wołczański
:::

```c=
#include "csapp.h"
static void signal_handler(int signum, siginfo_t *info, void *data) {
if (signum == SIGINT) {
safe_printf("(%d) Screw you guys... I'm going home!\n", getpid());
_exit(0);
}
}
static void play(pid_t next, const sigset_t *set) {
for (;;) {
printf("(%d) Waiting for a ball!\n", getpid());
/* TODO: Something is missing here! */
Sigsuspend(set);
usleep((300 + random() % 400) * 1000);
Kill(next, SIGUSR1);
printf("(%d) Passing ball to (%d)!\n", getpid(), next);
}
}
int main(int argc, char *argv[]) {
if (argc != 2)
app_error("Usage: %s [CHILDREN]", argv[0]);
int children = atoi(argv[1]);
if (children < 4 || children > 20)
app_error("Give number of children in range from 4 to 20!");
/* Register signal handler for SIGUSR1 */
struct sigaction action = {.sa_sigaction = signal_handler};
Sigaction(SIGINT, &action, NULL);
Sigaction(SIGUSR1, &action, NULL);
/* TODO: Start all processes and make them wait for the ball! */
sigset_t mask, prev_mask;
sigemptyset(&mask);
sigaddset(&mask, SIGUSR1);
Sigprocmask(SIG_BLOCK, &mask, &prev_mask);
pid_t next = getpid();
for(int i = 0; i < children; i++) {
pid_t pid = Fork();
if(pid == 0) {
play(next, &prev_mask);
}
printf("(%d) Child (%d) created!\n", getpid(), pid);
next = pid;
}
Kill(next, SIGUSR1);
play(next, &prev_mask);
return EXIT_SUCCESS;
}
}
```
Piłki mogą skleić się w jedną. Wystarczy, że jeden proces otrzyma dwie piłki przed tym, jak zostanie wybudzony z `sigsuspend`.
## Zadanie 8
:::success
Autor: Miłosz Urbanik
:::
* procedura obsługi sygnału - procedura przypisana do odpowiedniego sygnału funkcją `int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);` przez strukturę `sigaction`. Wywołana po otrzymaniu sygnału.
* strona wirtualna - ciągły blok pamięci wirtualnej, mapowany do stron pamięci rzeczywistej
* błąd strony - przerwanie spowodowane próbą dostępu do pamięci, która nie została wczytana do RAM
* pamięć wirtualna - abstrakcja pamięci dostępnej dla procesu
* procedury wielobieżne (reentrant functions, async-signal safe) - funkcje, które mogą być bezpiecznie zatrzymywane i wielokrotnie uruchamiane z różnych wątków. Charaketryzują się tym, że nie korzystają ze zmiennych globalnych, ani lokalnych zmiennych statycznych, wszystkie wywołane przez nie funkcje też są wielobieżne.
Z manuala `sigaction` struktura zawierająca informacje o sygnale
```c=
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
union sigval si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count;
POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since Linux 2.6.32) */
void *si_lower; /* Lower bound when address violation
occurred (since Linux 3.19) */
void *si_upper; /* Upper bound when address violation
occurred (since Linux 3.19) */
int si_pkey; /* Protection key on PTE that caused
fault (since Linux 4.6) */
void *si_call_addr; /* Address of system call instruction
(since Linux 3.5) */
int si_syscall; /* Number of attempted system call
(since Linux 3.5) */
unsigned int si_arch; /* Architecture of attempted system call
(since Linux 3.5) */
}
```
Manual `getcontext`
```c=
typedef struct ucontext_t {
struct ucontext_t *uc_link;
sigset_t uc_sigmask;
stack_t uc_stack;
mcontext_t uc_mcontext;
...
} ucontext_t;
```
Z `/usr/lib/x86_64-linux-gnu/sys/ucontext.h`
```c=
/* Context to describe whole processor state. */
typedef struct
{
gregset_t __ctx(gregs);
/* Note that fpregs is a pointer. */
fpregset_t __ctx(fpregs);
__extension__ unsigned long long __reserved1 [8];
} mcontext_t;
/* Userlevel context. */
typedef struct ucontext_t
{
unsigned long int __ctx(uc_flags);
struct ucontext_t *uc_link;
stack_t uc_stack;
mcontext_t uc_mcontext;
sigset_t uc_sigmask;
struct _libc_fpstate __fpregs_mem;
__extension__ unsigned long long int __ssp[4];
} ucontext_t;
```
Kod zmodyfikowanego handlera sygnału SIGSEGV
```c=
static void sigsegv_handler(int signum, siginfo_t *info, void *data) {
ucontext_t *uc = data;
intptr_t rip;
/* TODO: You need to get value of instruction pointer register from `uc`.
* Print all useful data from `info` and quit in such a way that a shell
* reports program has been terminated with SIGSEGV. */
rip = uc->uc_mcontext.gregs[17]; //enum w ucontext.h RIP_REG, gregs to tablica General REGisterS
/*
* SIGILL, SIGFPE, SIGSEGV, SIGBUS, and SIGTRAP fill in si_addr with the address of the fault. On some architectures,
these signals also fill in the si_trapno field.
*/
void* accs_addr = info->si_addr;
int err_code = info->si_code;
if(accs_addr > ADDR_START && accs_addr < ADDR_END) {
void* page_addr = (void*)((long)accs_addr - ((long)accs_addr % pagesize));
if(err_code == SEGV_MAPERR) {
safe_printf("Fault at rip=%lx accessing %lx! Map missing page at %lx.\n", (long)rip, (long)accs_addr, (long)page_addr);
mmap_page(page_addr, PROT_WRITE);
} else if(err_code == SEGV_ACCERR) {
safe_printf("Fault at rip=%lx accessing %lx! Make page at %lx writable.\n", (long)rip, (long)accs_addr, (long)page_addr);
mprotect_page(page_addr, PROT_WRITE);
}
} else {
safe_printf("Fault at rip=%lx accessing %lx! Address not mapped - terminating!\n", (long)rip, (long)accs_addr);
exit(139);
}
}
```