cpointersminix

Function pointers pointing to fixed addresses


I was looking through the MINIX 3 headers and in include/signal.h there was some seemingly unusual definitions:

/* Macros used as function pointers */
#define SIG_ERR ((sig_handler_t) -1)    /* error return */
#define SIG_DFL ((sig_handler_t) 0)     /* default signal handling */
#define SIG_IGN ((sig_handler_t) 1)     /* ignore signal */
#define SIG_HOLD ((sig_handler_t) 2)    /* block signal */ 
#define SIG_CATCH ((sig_handler_t) 3)   /* catch signal */

First of all, how is SIG_ERR valid? Second of all (and for my main question), there have been other pointers in the source code that map to some of those addresses (ex. NULL). What would happen if you dereference one of these pointers? Is the data in those addresses valid?


Solution

  • These are not memory addresses and dereferencing them is not meaningful. They are “magic” values that indicate that this is not the address of a handler function, but an instruction to set the signal handling state to something other than “run this function”.

    The values are chosen to be distinct from the address of any valid function, because otherwise there'd be no way to tell whether the caller of the signal function passed the address of a function or the magic value. Virtually all systems that have an MMU arrange to map nothing in the first page, so addresses below the page size cannot be the address of a variable of function. This enables NULL to be the address 0, for example.

    The value -1 is typically mapped to the highest possible address (all-bits-one), just like (unsigned)(-1) is all-bits-one. But that's an implementation choice (unlike (unsigned)(-1), which is perfectly well-defined since unsigned integers are defined modulo 2N where N is the bit size). For example, on some implementations where int is a 32-bit type but addresses have 64 bits, ((sig_handler_t) -1) would map to the address 0xffffffff, which is a plausible address for a function.

    Note that these are things that the operating system implementer can do, because they know how pointers are represented on a particular platform. The representation of pointers is not specified by the C standard (specifically, the effect of converting an integer to a pointer is implementation-defined) and constraints vary from system to system. As a C programmer, you can't do this (more precisely: you can, but unless you know exactly what you're doing, it's likely to go wrong). Not only would you have to know how a particular platform represents pointers and how it converts integers to pointers, but you'd also have to know what assumptions the compiler makes on your code. OS code may need to be compiled with a specific compiler or with specific compiler flags to enable the necessary implementation-specific behavior.

    The signal system call uses them in a way like the following (vastly simplified, but you get the idea):

    enum signal_disposition {
        SIGNAL_DISPOSITION_IGNORE,
        SIGNAL_DISPOSITION_KILL,
        SIGNAL_DISPOSITION_RUN_HANDLER,
        SIGNAL_DISPOSITION_STOP,
    };
    
    sighandler_t sys_signal(struct task *calling_task, int signum, sighandler_t handler)
    {
        if (signum > SIGMAX || signum == SIGKILL) return SIG_ERR;
        sighandler_t previous_handler =
            calling_task->signal_disposition == SIGNAL_DISPOSITION_IGNORE ? SIG_IGN :
            calling_task->signal_disposition == SIGNAL_DISPOSITION_RUN_HANDLER ?
            calling_task->signal_handler[signum] :
            SIG_DFL;
        if (handler == SIG_DFL) {
            calling_task->signal_disposition[signum] =
                signum == SIGTSTP ? SIGNAL_DISPOSITION_STOP :
                signum == SIGALRM ? SIGNAL_DISPOSITION_IGNORE :
                SIGNAL_DISPOTITION_KILL;
            calling_task->signal_handler[signum] = NULL;
        } else if (handler == SIG_IGN) {
            calling_task->signal_disposition[signum] = SIGNAL_DISPOSITION_IGNORE;
            calling_task->signal_handler[signum] = NULL;
        } else {
            calling_task->signal_disposition[signum] = SIGNAL_DISPOSITION_RUN_HANDLER;
            calling_task->signal_handler[signum] = handler;
        }
        return previous_handler;
    }
    

    And this is the corresponding code run in the kernel to trigger a signal in a process (again, vastly simplified):

    void handle_signal(struct task *calling_task, int signum) {
        switch (calling_task->signal_disposition[signum]) {
        case SIGNAL_DISPOSITION_IGNORE:
            break;
        case SIGNAL_DISPOSITION_KILL:
            kill_task(task, signum);
            break;
        case SIGNAL_DISPOSITION_RUN_HANDLER:
            task->registers->r0 = signum;
            task->registers->pc = calling_task->signal_handler;
            wake_up(task);
            break;
        case SIGNAL_DISPOSITION_STOP:
            stop_task(task);
            break;
        }
    }