wake-up-neo.com

Wie schreibe ich einen Signal Handler um SIGSEGV zu fangen?

Ich möchte einen Signalhandler schreiben, um SIGSEGV zu fangen. Ich schütze einen Speicherblock zum Lesen oder Schreiben mit

char *buffer;
char *p;
char a;
int pagesize = 4096;

mprotect(buffer,pagesize,PROT_NONE)

Dadurch werden die Bytes des Arbeitsspeichers der Seitengröße ab Puffer vor Lese- und Schreibvorgängen geschützt.

Zweitens versuche ich, den Speicher zu lesen:

p = buffer;
a = *p 

Dadurch wird eine SIGSEGV generiert, und mein Handler wird aufgerufen. So weit, ist es gut. Mein Problem ist, dass ich nach dem Aufruf des Handlers das Zugriffsschreiben des Speichers dadurch ändern möchte 

mprotect(buffer,pagesize,PROT_READ);

und normal weiter funktionieren meines Codes. Ich möchte die Funktion nicht beenden. Bei zukünftigen Schreibvorgängen in den gleichen Speicher möchte ich das Signal erneut einfangen und die Schreibrechte ändern und dann dieses Ereignis aufzeichnen.

Hier ist der Code :

#include <signal.h>
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/mman.h>

#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)

char *buffer;
int flag=0;

static void handler(int sig, siginfo_t *si, void *unused)
{
    printf("Got SIGSEGV at address: 0x%lx\n",(long) si->si_addr);
    printf("Implements the handler only\n");
    flag=1;
    //exit(EXIT_FAILURE);
}

int main(int argc, char *argv[])
{
    char *p; char a;
    int pagesize;
    struct sigaction sa;

    sa.sa_flags = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = handler;
    if (sigaction(SIGSEGV, &sa, NULL) == -1)
        handle_error("sigaction");

    pagesize=4096;

    /* Allocate a buffer aligned on a page boundary;
       initial protection is PROT_READ | PROT_WRITE */

    buffer = memalign(pagesize, 4 * pagesize);
    if (buffer == NULL)
        handle_error("memalign");

    printf("Start of region:        0x%lx\n", (long) buffer);
    printf("Start of region:        0x%lx\n", (long) buffer+pagesize);
    printf("Start of region:        0x%lx\n", (long) buffer+2*pagesize);
    printf("Start of region:        0x%lx\n", (long) buffer+3*pagesize);
    //if (mprotect(buffer + pagesize * 0, pagesize,PROT_NONE) == -1)
    if (mprotect(buffer + pagesize * 0, pagesize,PROT_NONE) == -1)
        handle_error("mprotect");

    //for (p = buffer ; ; )
    if(flag==0)
    {
        p = buffer+pagesize/2;
        printf("It comes here before reading memory\n");
        a = *p; //trying to read the memory
        printf("It comes here after reading memory\n");
    }
    else
    {
        if (mprotect(buffer + pagesize * 0, pagesize,PROT_READ) == -1)
        handle_error("mprotect");
        a = *p;
        printf("Now i can read the memory\n");

    }
/*  for (p = buffer;p<=buffer+4*pagesize ;p++ ) 
    {
        //a = *(p);
        *(p) = 'a';
        printf("Writing at address %p\n",p);

    }*/

    printf("Loop completed\n");     /* Should never happen */
    exit(EXIT_SUCCESS);
}

Das Problem ist, dass nur der Signal-Handler ausgeführt wird und ich nach dem Erfassen des Signals nicht zur Hauptfunktion zurückkehren kann.

64
Adi

Wenn Ihr Signal-Handler zurückkehrt (vorausgesetzt, er ruft nicht exit oder longjmp auf oder etwas, das die eigentliche Rückkehr verhindert), wird der Code an der Stelle fortgesetzt, an der das Signal aufgetreten ist, und dieselbe Anweisung erneut ausführen. Da zu diesem Zeitpunkt der Speicherschutz nicht geändert wurde, wird das Signal einfach erneut ausgelöst, und Sie befinden sich in einer Endlosschleife wieder im Signalhandler.

Damit dies funktioniert, müssen Sie mprotect im Signalhandler aufrufen. Wie Steven Schansker feststellt, ist mprotect leider nicht asynchron, daher können Sie es nicht sicher vom Signalhandler aus anrufen. Was POSIX angeht, sind Sie also vermasselt.

Glücklicherweise ist mprotect bei den meisten Implementierungen (alle modernen UNIX- und Linux-Varianten, soweit ich weiß) ein Systemaufruf. Daher ist sicher innerhalb eines Signal-Handlers aufzurufen , sodass Sie das meiste tun können, was Sie möchten. Das Problem ist, dass, wenn Sie den Schutz nach dem Lesen wieder ändern möchten, dies nach dem Lesen im Hauptprogramm geschehen muss.

Eine andere Möglichkeit besteht darin, mit dem dritten Argument etwas an den Signal-Handler zu tun, der auf eine OS- und Arch-spezifische Struktur verweist, die Informationen darüber enthält, wo das Signal aufgetreten ist. Unter Linux ist dies eine ucontext -Struktur, die maschinenspezifische Informationen über die $ PC-Adresse und andere Registerinhalte enthält, in denen das Signal aufgetreten ist. Wenn Sie dies ändern, ändern Sie den Ort, an den der Signal-Handler zurückkehren wird, sodass Sie den $ PC so ändern können, dass er unmittelbar nach der fehlerhaften Anweisung ist, damit er nach der Rückkehr des Handlers nicht erneut ausgeführt wird. Dies ist sehr schwierig, um richtig zu werden (und auch nicht portabel).

edit

Die ucontext-Struktur ist in <ucontext.h> definiert. Innerhalb der Variablen ucontext enthält das Feld uc_mcontext den Maschinenkontext und innerhalb von that das Array gregs den allgemeinen Registerkontext. Also in deinem Signalhandler:

ucontext *u = (ucontext *)unused;
unsigned char *pc = (unsigned char *)u->uc_mcontext.gregs[REG_RIP];

erhalten Sie den PC, auf dem die Ausnahme aufgetreten ist. Sie können es lesen, um herauszufinden, welche Anweisung es fehlerhaft war, und etwas anderes zu tun.

Was die Portabilität des Aufrufs von mprotect im Signalhandler betrifft, sollte jedes System, das entweder der SVID-Spezifikation oder der BSD4-Spezifikation folgt, sicher sein - es ermöglicht den Aufruf eines beliebigen Systemaufrufs (alles in Abschnitt 2 des Handbuchs) in einem Signal Handler.

63
Chris Dodd

Sie sind in die Falle gegangen, die alle Menschen beim ersten Versuch unternehmen, mit Signalen umzugehen. Die Falle? Denke, dass du mit Signal-Handlern tatsächlich alles Nützliche machen kannst . Von einem Signalhandler aus dürfen Sie nur asynchrone und wiedereintrittssichere Bibliotheksaufrufe aufrufen.

Siehe diese CERT-Empfehlung , warum und eine Liste der POSIX-Funktionen, die sicher sind.

Beachten Sie, dass sich printf (), das Sie bereits aufrufen, nicht in dieser Liste befindet.

Auch ist mprotect. Sie dürfen es nicht von einem Signal-Handler aus aufrufen. Es könnte funktionieren, aber ich kann versprechen, dass Sie später auf Probleme stoßen werden. Seien Sie sehr vorsichtig mit Signal-Handlern, denn es ist schwierig, die richtigen zu finden!

EDIT

Da ich im Moment bereits ein Douchebag für Portabilität bin, möchte ich darauf hinweisen, dass Sie auch nicht in gemeinsam genutzte (d. H. Globale) Variablen schreiben sollten , ohne die richtigen Vorsichtsmaßnahmen zu treffen.

24

Sie können SIGSEGV unter Linux wiederherstellen. Unter Windows können Sie auch Segmentierungsfehler beheben (statt eines Signals sehen Sie eine strukturierte Ausnahme). Aber der POSIX-Standard garantiert keine Wiederherstellung , daher wird Ihr Code nicht portierbar sein.

Werfen Sie einen Blick auf libsigsegv .

11
Ben Voigt

Sie sollten nicht vom Signalhandler zurückkehren, da das Verhalten dann undefiniert ist. Springe lieber mit longjmp raus. 

Dies ist nur in Ordnung, wenn das Signal asynchron generiert wird. Andernfalls ist das Verhalten undefiniert, wenn das Programm jemals eine andere async-Signal unsichere Funktion aufruft. Daher sollte der Signal-Handler nur unmittelbar vor seiner Einrichtung eingerichtet und so schnell wie möglich gelöscht werden.

Ich kenne eigentlich nur wenige Einsatzmöglichkeiten eines SIGSEGV-Handlers:

  • verwenden Sie eine asyncsignalsichere Rückverfolgungsbibliothek, um eine Rückverfolgung zu protokollieren, und sterben Sie dann.
  • in einem VM wie der JVM oder der CLR: Überprüfen Sie, ob der SIGSEGV in JIT-kompiliertem Code aufgetreten ist. Wenn nicht, stirb; Wenn dies der Fall ist, werfen Sie eine sprachspezifische Ausnahme ( nicht eine C++ - Ausnahme) aus. Dies funktioniert, weil der JIT-Compiler wusste, dass der Trap auftreten konnte, und entsprechende Frame-Abwicklungsdaten generiert hat.
  • clone () und exec () einen Debugger (do not use fork () - der Callbacks aufruft, die von pthread_atfork () registriert sind).

Beachten Sie schließlich, dass jede Aktion, die SIGSEGV auslöst, wahrscheinlich eine UB ist, da auf ungültigen Speicher zugegriffen wird. Dies wäre jedoch nicht der Fall, wenn das Signal beispielsweise SIGFPE wäre. 

4
Demi

Es gibt ein Kompilierungsproblem mit ucontext_t oder struct ucontext (in /usr/include/sys/ucontext.h vorhanden)

http://www.mail-archive.com/[email protected]/msg13853.html

0
shreshtha