Signals: Interrupts in userspace

    "Guido here is going to send you a signal.  He's going to set your car on fire."
        -- hypothetical OS Mafia don

A signal is when the OS calls you:

A signal is usually not something you can ignore--the default signal handler usually exits your process.

Signals are available on all POSIX operating systems (including Windows, Linux, Mac OS X), and include many program errors:

Signals can also be used to indicate that I/O is ready (SIGIO, enabled using ``fcntl''), that a timer has expired (SIGALRM, SIGPROF, or SIGVPROF, enabled using ``setitimer''), that the operating system wants you to shut down (SIGTERM, SIGQUIT, SIGKILL, all UNIX-specific), that various events have happened on the terminal (SIGHUP, SIGWINCH, SIGPIPE, SIGTTIN, SIGTTOU, all UNIX-specific), or for application-defined purposes (SIGUSR1/SIGUSR2, which must be sent explicitly).  See signal.h for the full list of signals.

Signals, exactly like interrupts, are hence a generic ``catch-all'' notification mechanism, used for a variety of unrelated tasks.

Writing a Signal Handler

Signals can be seen as a standardized interface for delivering interrupts to user programs. Exactly like interrupts, a signal handler is just a subroutine that gets called when something weird happens.

Overall signal delivery looks like this:

  1. Something causes an interrupt--a hardware device needs attention, or a program reads a bad memory address, divides by zero, executes an illegal or privileged instruction, etc.
  2. The CPU looks up the OS interrupt service routine in the interrupt table (or "interrupt vector", for some strange reason.)
  3. The OS's interrupt service routine figures out if it can handle the interrupt, or if it should deliver the interrupt to a process as a signal.
  4. To deliver a signal, the OS essentially just calls your process's subroutine.

To set yourself up to receive signals (add a signal handler), you just call an operating system routine like signal. You pass in the name of the signal you want to receive, and a function to execute once the signal is received.  For example:

#include <signal.h>

void myHandler(int i)
{
printf("Sorry dude--you just hit signal %d\n",i);
exit(1);
}

int foo(void) {
int *badPointer=(int *)0;
printf("Installing signal handler\n");
signal(SIGSEGV,myHandler); /* <------------- */
printf("Signal handler installed. Segfaulting...\n");
(*badPointer)++;
printf("Back from segfault?!\n");
return 0;
}
(Executable NetRun Link)

Which on my machine prints out:

Installing signal handler
Signal handler installed. Segfaulting...
Sorry dude--you just hit signal 11

Typically you do *not* want to just return from a signal handler without actually handling the problem.

On UNIX machines, there's also a slightly more sophisticated interface called sigaction.  The signal handler function for sigaction can take a siginfo_t, which includes information about the bad address:

#include <signal.h>
#ifdef SOLARIS /* needed with at least Solaris 8 */
#include <siginfo.h>
#endif

void segfaultHandler(int cause, siginfo_t *HowCome, void *ucontext_void) {
void *badptr=HowCome->si_addr;
std::cout<<"Bad pointer access at address "<<badptr<<"\n";
exit(1); // the easy way out: just exit.
}

int foo(void)
{
/* Install our SIGSEGV signal handler */
struct sigaction sa;
sa.sa_sigaction = segfaultHandler;
sigemptyset( &sa.sa_mask );
sa.sa_flags = SA_SIGINFO; /* we want a siginfo_t */
if (sigaction (SIGSEGV, &sa, 0)) { perror("sigaction failed"); exit(1); }

/* Do something that crashes */
int *ptr=(int *)0x123456;
std::cout<<"About to access pointer "<<ptr<<"\n";
*ptr=3;
std::cout<<"Wait, that worked?!\n";

return 0;
}

(Try this in NetRun now!)

 

You can actually use the signal handler to fix the root cause of the error.  In this case, the problem was there's no memory at pointer 0x123456.  But using mmap, we can *make* there be memory at that address!

#include <sys/mman.h> /* for mmap */
#include <signal.h>
#ifdef SOLARIS /* needed with at least Solaris 8 */
#include <siginfo.h>
#endif

void segfaultHandler(int cause, siginfo_t *HowCome, void *ucontext_void) {
void *badptr=HowCome->si_addr;
std::cout<<"Fixing bad pointer access at address "<<badptr<<"\n";

long start=(long)badptr, end=16+(long)badptr;
start&=~0xfff; end+=0xfff; end&=~0xfff; /* round to page boundaries */
void *ret=mmap((void *)start,end-start,PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_ANONYMOUS|MAP_SHARED|MAP_FIXED,-1,0);
if (ret!=(void *)start) {
std::cout<<"Failed to map region (got "<<ret<<")\n";
exit(1);
}
/* else we've fixed the bad pointer, so return and keep running! */
}

(Try this in NetRun now!)

Segfault-fix-continue is a surprisingly powerful technique.  It's used by the operating system to implement virtual memory, used by dynamic translators to lazily demand-translate binary code, used by parallel programmers to fake shared memory across the network, and lots of other interesting strange things!

 


CS 301 Lecture Note, 2014, Dr. Orion LawlorUAF Computer Science Department.