Wie kann man einen Segmentierungsfehler unter Linux abfangen?

83

Ich muss einen Segmentierungsfehler bei Bibliotheksbereinigungsvorgängen von Drittanbietern abfangen. Dies geschieht manchmal kurz vor dem Beenden meines Programms, und ich kann den wahren Grund dafür nicht ermitteln. In der Windows-Programmierung könnte ich dies mit __try - __catch tun. Gibt es eine plattformübergreifende oder plattformspezifische Möglichkeit, dasselbe zu tun? Ich brauche das unter Linux, gcc.

Alex F.
quelle
Segmentierungsfehler werden immer durch einen Fehler verursacht, der sehr schwer zu erkennen sein kann. Ich finde nur eine, die zufällig erscheint. Jede Datei enthält 500 Millionen Datenpunkte. Ungefähr alle 10-15 Dateien tritt dieser Segmentierungsfehler auf. Ich habe Multithreading, sperrenfreie Warteschlangen usw. verwendet. Ziemlich kompliziertes Jobmanagement. Am Ende ist es ein Objekt, das ich erstellt habe, das std :: move () in eine andere Datenstruktur. Vor Ort habe ich dieses Objekt nach dem Umzug verwendet. Aus irgendeinem Grund ist C ++ damit einverstanden. Aber der Segfault wird sicher irgendwann auftauchen.
Kemin Zhou

Antworten:

80

Unter Linux können wir diese auch als Ausnahmen haben.

Wenn Ihr Programm einen Segmentierungsfehler ausführt, wird normalerweise ein SIGSEGVSignal gesendet . Sie können einen eigenen Handler für dieses Signal einrichten und die Folgen abmildern. Natürlich sollten Sie wirklich sicher sein , dass Sie kann aus der Situation zu erholen. In Ihrem Fall sollten Sie stattdessen Ihren Code debuggen.

Zurück zum Thema. Ich bin kürzlich auf eine Bibliothek ( kurzes Handbuch ) gestoßen , die solche Signale in Ausnahmen umwandelt, sodass Sie Code wie folgt schreiben können:

try
{
    *(int*) 0 = 0;
}
catch (std::exception& e)
{
    std::cerr << "Exception caught : " << e.what() << std::endl;
}

Ich habe es aber nicht überprüft. Funktioniert auf meiner x86-64 Gentoo Box. Es verfügt über ein plattformspezifisches Backend (aus der Java-Implementierung von gcc entlehnt), sodass es auf vielen Plattformen funktionieren kann. Es unterstützt nur x86 und x86-64, aber Sie können Backends von libjava erhalten, das sich in gcc-Quellen befindet.

P Shved
quelle
16
+1 für sicher, dass Sie sich erholen können, bevor Sie Sig Segfault
Henrik Mühe
16
Das Werfen von einem Signalhandler ist eine sehr gefährliche Sache. Die meisten Compiler gehen davon aus, dass nur Aufrufe Ausnahmen generieren können, und richten die Abwicklungsinformationen entsprechend ein. Sprachen, die Hardware-Ausnahmen in Software-Ausnahmen umwandeln, wie Java und C #, sind sich bewusst, dass alles ausgelöst werden kann. Dies ist bei C ++ nicht der Fall. Bei GCC müssen Sie zumindest -fnon-call-exceptionssicherstellen, dass es funktioniert - und dies verursacht Leistungskosten. Es besteht auch die Gefahr, dass Sie von einer Funktion ohne Ausnahmeunterstützung (wie einer C-Funktion) geworfen werden und später auslaufen / abstürzen.
zneak
1
Ich stimme zneak zu. Werfen Sie nicht von einem Signalhandler.
MM.
Die Bibliothek befindet sich jetzt in github.com/Plaristote/segvcatch , aber ich konnte das Handbuch nicht finden oder kompilieren. ./build_gcc_linux_releasegibt mehrere Fehler.
AlfC
Yay! Jetzt weiß ich, dass ich nicht der einzige Gentoo-Benutzer auf der Welt bin!
SS Anne
46

Hier ist ein Beispiel, wie es in C gemacht wird.

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void segfault_sigaction(int signal, siginfo_t *si, void *arg)
{
    printf("Caught segfault at address %p\n", si->si_addr);
    exit(0);
}

int main(void)
{
    int *foo = NULL;
    struct sigaction sa;

    memset(&sa, 0, sizeof(struct sigaction));
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = segfault_sigaction;
    sa.sa_flags   = SA_SIGINFO;

    sigaction(SIGSEGV, &sa, NULL);

    /* Cause a seg fault */
    *foo = 1;

    return 0;
}
JayM
quelle
9
sizeof (sigaction) ==> sizeof (struct sigaction), oder Sie erhalten einen ISO C ++ - Fehler beim Kompilieren des Dings.
Dave Dopson
7
Das Ausführen von E / A in einem Signalhandler ist ein Rezept für eine Katastrophe.
Tim Seguine
6
@ TimSeguine: das stimmt nicht. Sie müssen nur sicherstellen, dass Sie wissen, was Sie tun. signal(7)listet alle async-signal-sicheren Funktionen auf, die mit relativ wenig Sorgfalt verwendet werden können. Im obigen Beispiel ist es auch völlig sicher, da nichts anderes im Programm berührt stdoutals der printfAufruf im Handler.
stefanct
3
@stefanct Dies ist ein Spielzeugbeispiel. Praktisch jedes Nicht-Spielzeug-Programm wird irgendwann die Sperre für stdout halten. Mit diesem Signalhandler ist das Schlimmste, was wahrscheinlich passieren kann, ein Deadlock bei Segfault. Dies kann jedoch schlimm genug sein, wenn Sie derzeit keinen Mechanismus haben, um unerwünschte Prozesse in Ihrem Anwendungsfall abzubrechen.
Tim Seguine
3
Gemäß 2.4.3 Signalaktionen ist das Aufrufen von printf aus einem Signalhandler heraus, der als Ergebnis einer illegalen Indirektion aufgerufen wird, unabhängig davon, ob das Programm Multithreading ist oder nicht, nur eine undefinierte Verhaltensperiode .
Julien Villemure-Fréchette
9

Aus Gründen der Portabilität sollte man wahrscheinlich std::signaldie Standard-C ++ - Bibliothek verwenden, aber es gibt viele Einschränkungen, was ein Signalhandler tun kann. Leider ist es nicht möglich, eine SIGSEGV aus einem C ++ - Programm heraus abzufangen , ohne ein undefiniertes Verhalten einzuführen, da in der Spezifikation Folgendes angegeben ist:

  1. es ist nicht definiertes Verhalten jede Bibliotheksfunktion innerhalb der Handler außer einem sehr engen Teilmenge der Standardbibliothek Funktionen aufrufen ( abort, exiteinige Atomfunktionen, Stromsignal - Handler installieren, memcpy, memmove, Typ Züge, `std :: bewegen , std::forward, und einige mehr ).
  2. Es ist ein undefiniertes Verhalten, wenn der Handler einen throwAusdruck verwendet.
  3. Es ist ein undefiniertes Verhalten, wenn der Handler bei der Verarbeitung von SIGFPE, SIGILL, SIGSEGV zurückkehrt

Dies beweist, dass es unmöglich ist, SIGSEGV innerhalb eines Programms mit streng standardmäßigem und portablem C ++ abzufangen. SIGSEGV ist nach wie vor durch das Betriebssystem gefangen und wird in der Regel an dem übergeordneten Prozess berichtet , wenn eine Warte Familie Funktion aufgerufen wird.

Bei der Verwendung des POSIX-Signals werden Sie wahrscheinlich auf die gleichen Probleme stoßen, da in 2.4.3 Signalaktionen eine Klausel steht :

Das Verhalten eines Prozesses ist nicht definiert , nachdem es in der Regel aus einem Signal Fang - Funktion für einen SIGBUS, SIGFPE, SIGILL oder SIGSEGV Signal zurückgibt , die nicht von generiert wurde kill(), sigqueue()oder raise().

Ein Wort zum longjumps. Angenommen, wir verwenden POSIX-Signale, longjumphilft es nicht, das Abwickeln des Stapels zu simulieren:

Obwohl longjmp()es sich um eine asynchronsignalsichere Funktion handelt, wird die Funktion aufgerufen, wenn sie von einem Signalhandler aufgerufen wird, der eine nicht asynchronsignalsichere Funktion oder eine gleichwertige Funktion unterbrochen hat (z. B. die Verarbeitung, exit()die nach einer Rückkehr vom ersten Aufruf an ausgeführt wird main()) Das Verhalten eines nachfolgenden Aufrufs einer nicht asynchronen signal-sicheren Funktion oder einer gleichwertigen Funktion ist undefiniert.

Dies bedeutet, dass die Fortsetzung, die durch den Aufruf von longjump aufgerufen wird, normalerweise nützliche Bibliotheksfunktionen wie oder oder von main nicht zuverlässig aufrufen kann printf, ohne ein undefiniertes Verhalten hervorzurufen . Als solches kann die Fortsetzung nur eingeschränkte Operationen ausführen und kann nur durch einen abnormalen Beendigungsmechanismus beendet werden.mallocexit

Um es kurz zu machen, ein SIGSEGV abzufangen und die Ausführung des Programms in einem tragbaren Gerät fortzusetzen, ist wahrscheinlich nicht möglich, ohne UB einzuführen. Selbst wenn Sie auf einer Windows-Plattform arbeiten, für die Sie Zugriff auf die Behandlung strukturierter Ausnahmen haben, ist es erwähnenswert, dass MSDN empfiehlt, niemals zu versuchen, Hardware-Ausnahmen zu behandeln: Hardware-Ausnahmen

Julien Villemure-Fréchette
quelle
SIGSEGV ist jedoch kaum eine Hardware-Ausnahme. Man könnte immer eine Eltern-Kind-Architektur verwenden, bei der der Elternteil den Fall eines vom Kernel getöteten Kindes erkennen und IPC verwenden kann, um den relevanten Programmstatus zu teilen, um dort fortzufahren, wo wir abgereist sind. Ich glaube, moderne Browser können so gesehen werden, da sie IPC-Mechanismen verwenden, um mit diesem einen Prozess pro Browser-Registerkarte zu kommunizieren. Offensichtlich ist die Sicherheitsgrenze zwischen Prozessen ein Bonus im Browserszenario.
0xC0000022L
8

C ++ - Lösung finden Sie hier ( http://www.cplusplus.com/forum/unices/16430/ )

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void ouch(int sig)
{
    printf("OUCH! - I got signal %d\n", sig);
}
int main()
{
    struct sigaction act;
    act.sa_handler = ouch;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGINT, &act, 0);
    while(1) {
        printf("Hello World!\n");
        sleep(1);
    }
}
revo
quelle
7
Ich weiß, dass dies nur ein Beispiel ist, das Sie nicht geschrieben haben, aber IO in einem Signalhandler auszuführen, ist ein Rezept für eine Katastrophe.
Tim Seguine
3
@ TimSeguine: Wiederholen von Dingen, die bestenfalls sehr irreführend sind, ist keine gute Idee (vgl. Stackoverflow.com/questions/2350489/… )
stefanct
3
@stefanct Die Vorsichtsmaßnahmen, die erforderlich sind, um printf sicher in einem Signalhandler zu verwenden, sind nicht trivial. Daran ist nichts Irreführendes. Dies ist ein Spielzeugbeispiel. Und selbst in diesem Spielzeugbeispiel ist es möglich, einen Deadlock zu verursachen, wenn Sie den SIGINT richtig einstellen. Deadlocks sind gerade deshalb gefährlich, weil sie selten sind. Wenn Sie der Meinung sind, dass dieser Rat irreführend war, halten Sie sich von meinem Code fern, da ich Ihnen nicht innerhalb einer Meile davon vertraue.
Tim Seguine
Auch hier haben Sie allgemein über E / A gesprochen. Anstatt auf das Problem mit diesem tatsächlichen Beispiel hinzuweisen, das in der Tat ein schlechtes ist.
stefanct
1
@stefanct Wenn Sie den Kontext der Anweisung nicht auswählen und ignorieren möchten, ist dies Ihr Problem. Wer hat gesagt, dass ich allgemein über E / A spreche? Sie. Ich habe nur ein großes Problem mit Leuten, die Spielzeugantworten auf schwierige Probleme posten. Selbst wenn Sie asynchrone sichere Funktionen verwenden, gibt es noch viel zu überlegen, und diese Antwort lässt es als trivial erscheinen.
Tim Seguine
5

Manchmal möchten wir a abfangen SIGSEGV, um herauszufinden, ob ein Zeiger gültig ist, dh ob er auf eine gültige Speicheradresse verweist. (Oder überprüfen Sie sogar, ob ein beliebiger Wert ein Zeiger sein kann.)

Eine Option ist, es mit zu überprüfen isValidPtr()(arbeitete auf Android):

int isValidPtr(const void*p, int len) {
    if (!p) {
    return 0;
    }
    int ret = 1;
    int nullfd = open("/dev/random", O_WRONLY);
    if (write(nullfd, p, len) < 0) {
    ret = 0;
    /* Not OK */
    }
    close(nullfd);
    return ret;
}
int isValidOrNullPtr(const void*p, int len) {
    return !p||isValidPtr(p, len);
}

Eine andere Möglichkeit besteht darin, die Speicherschutzattribute zu lesen, was etwas schwieriger ist (funktioniert unter Android):

re_mprot.c:

#include <errno.h>
#include <malloc.h>
//#define PAGE_SIZE 4096
#include "dlog.h"
#include "stdlib.h"
#include "re_mprot.h"

struct buffer {
    int pos;
    int size;
    char* mem;
};

char* _buf_reset(struct buffer*b) {
    b->mem[b->pos] = 0;
    b->pos = 0;
    return b->mem;
}

struct buffer* _new_buffer(int length) {
    struct buffer* res = malloc(sizeof(struct buffer)+length+4);
    res->pos = 0;
    res->size = length;
    res->mem = (void*)(res+1);
    return res;
}

int _buf_putchar(struct buffer*b, int c) {
    b->mem[b->pos++] = c;
    return b->pos >= b->size;
}

void show_mappings(void)
{
    DLOG("-----------------------------------------------\n");
    int a;
    FILE *f = fopen("/proc/self/maps", "r");
    struct buffer* b = _new_buffer(1024);
    while ((a = fgetc(f)) >= 0) {
    if (_buf_putchar(b,a) || a == '\n') {
        DLOG("/proc/self/maps: %s",_buf_reset(b));
    }
    }
    if (b->pos) {
    DLOG("/proc/self/maps: %s",_buf_reset(b));
    }
    free(b);
    fclose(f);
    DLOG("-----------------------------------------------\n");
}

unsigned int read_mprotection(void* addr) {
    int a;
    unsigned int res = MPROT_0;
    FILE *f = fopen("/proc/self/maps", "r");
    struct buffer* b = _new_buffer(1024);
    while ((a = fgetc(f)) >= 0) {
    if (_buf_putchar(b,a) || a == '\n') {
        char*end0 = (void*)0;
        unsigned long addr0 = strtoul(b->mem, &end0, 0x10);
        char*end1 = (void*)0;
        unsigned long addr1 = strtoul(end0+1, &end1, 0x10);
        if ((void*)addr0 < addr && addr < (void*)addr1) {
            res |= (end1+1)[0] == 'r' ? MPROT_R : 0;
            res |= (end1+1)[1] == 'w' ? MPROT_W : 0;
            res |= (end1+1)[2] == 'x' ? MPROT_X : 0;
            res |= (end1+1)[3] == 'p' ? MPROT_P
                 : (end1+1)[3] == 's' ? MPROT_S : 0;
            break;
        }
        _buf_reset(b);
    }
    }
    free(b);
    fclose(f);
    return res;
}

int has_mprotection(void* addr, unsigned int prot, unsigned int prot_mask) {
    unsigned prot1 = read_mprotection(addr);
    return (prot1 & prot_mask) == prot;
}

char* _mprot_tostring_(char*buf, unsigned int prot) {
    buf[0] = prot & MPROT_R ? 'r' : '-';
    buf[1] = prot & MPROT_W ? 'w' : '-';
    buf[2] = prot & MPROT_X ? 'x' : '-';
    buf[3] = prot & MPROT_S ? 's' : prot & MPROT_P ? 'p' :  '-';
    buf[4] = 0;
    return buf;
}

re_mprot.h:

#include <alloca.h>
#include "re_bits.h"
#include <sys/mman.h>

void show_mappings(void);

enum {
    MPROT_0 = 0, // not found at all
    MPROT_R = PROT_READ,                                 // readable
    MPROT_W = PROT_WRITE,                                // writable
    MPROT_X = PROT_EXEC,                                 // executable
    MPROT_S = FIRST_UNUSED_BIT(MPROT_R|MPROT_W|MPROT_X), // shared
    MPROT_P = MPROT_S<<1,                                // private
};

// returns a non-zero value if the address is mapped (because either MPROT_P or MPROT_S will be set for valid addresses)
unsigned int read_mprotection(void* addr);

// check memory protection against the mask
// returns true if all bits corresponding to non-zero bits in the mask
// are the same in prot and read_mprotection(addr)
int has_mprotection(void* addr, unsigned int prot, unsigned int prot_mask);

// convert the protection mask into a string. Uses alloca(), no need to free() the memory!
#define mprot_tostring(x) ( _mprot_tostring_( (char*)alloca(8) , (x) ) )
char* _mprot_tostring_(char*buf, unsigned int prot);

PS DLOG()ist printf()zum Android-Protokoll. FIRST_UNUSED_BIT()ist hier definiert .

PPS Es ist möglicherweise keine gute Idee, alloca () in einer Schleife aufzurufen. Der Speicher wird möglicherweise erst freigegeben, wenn die Funktion zurückgegeben wird.

18446744073709551615
quelle