Jak wychwycić błąd segmentacji w Linuksie?

84

Muszę wychwycić błąd segmentacji w operacjach czyszczenia bibliotek innych firm. Dzieje się to czasami tuż przed zamknięciem programu i nie potrafię naprawić prawdziwej przyczyny. W programowaniu Windows mogłem to zrobić za pomocą __try - __catch. Czy istnieje wieloplatformowy lub specyficzny dla platformy sposób, aby zrobić to samo? Potrzebuję tego w Linuksie, gcc.

Alex F.
źródło
Błąd segmentacji jest zawsze spowodowany błędem, który może być naprawdę trudny do wykrycia. Po prostu znajduję taki, który pojawia się losowo. Każdy plik zawiera 500 milionów punktów danych. Mniej więcej co 10-15 plików pojawia się ten błąd segmentacji. Używałem wielowątkowości, kolejki bez blokad itp. Dość skomplikowane zarządzanie zadaniami. Ostatecznie jest to obiekt, który utworzyłem, std :: przeniesiony () do innej struktury danych. Lokalnie korzystałem z tego obiektu po przeprowadzce. Z jakiegoś powodu C ++ jest w porządku z tym. Ale segfault na pewno pojawi się w pewnym momencie.
Kemin Zhou

Odpowiedzi:

80

W Linuksie możemy je również mieć jako wyjątki.

Zwykle, gdy program wykonuje błąd segmentacji, wysyłany jest SIGSEGVsygnał. Możesz ustawić własną obsługę dla tego sygnału i złagodzić konsekwencje. Oczywiście powinieneś być pewien, że możesz wyleczyć się z sytuacji. Myślę, że w twoim przypadku powinieneś zamiast tego debugować swój kod.

Wracając do tematu. Niedawno natknąłem się na bibliotekę ( krótką instrukcję ), która przekształca takie sygnały w wyjątki, więc możesz pisać taki kod:

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

Jednak nie sprawdziłem tego. Działa na moim Gentoo boxie x86-64. Ma zaplecze specyficzne dla platformy (zapożyczone z implementacji java gcc), więc może działać na wielu platformach. Po prostu obsługuje x86 i x86-64 po wyjęciu z pudełka, ale możesz pobrać backendy z libjava, która znajduje się w źródłach gcc.

P Shved
źródło
16
+1 dla pewności, że możesz odzyskać siły, zanim złapiesz sig segfault
Henrik Mühe,
16
Rzucanie przez osobę obsługującą sygnał jest bardzo niebezpieczną rzeczą. Większość kompilatorów zakłada, że ​​tylko wywołania mogą generować wyjątki i odpowiednio konfigurować rozwijanie informacji. Języki, które przekształcają wyjątki sprzętowe w wyjątki oprogramowania, takie jak Java i C #, są świadome, że wszystko może zostać rzucone; tak nie jest w przypadku C ++. W przypadku GCC musisz przynajmniej -fnon-call-exceptionsupewnić się, że działa - i wiąże się to z kosztem wydajności. Istnieje również niebezpieczeństwo, że będziesz wyrzucać z funkcji bez obsługi wyjątków (jak funkcja C), a później wycieknie / ulegnie awarii.
zneak
1
Zgadzam się z zneak. Nie rzucaj z uchwytu sygnału.
MM.
Biblioteka jest teraz pod adresem github.com/Plaristote/segvcatch , ale nie mogłem znaleźć instrukcji ani jej skompilować. ./build_gcc_linux_releasedaje kilka błędów.
alfC
Yay! Teraz wiem, że nie jestem jedynym użytkownikiem Gentoo na świecie!
SS Anne
46

Oto przykład, jak to zrobić w C.

#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
źródło
9
sizeof (sigaction) ==> sizeof (struct sigaction), albo otrzymujesz błąd ISO C ++ podczas kompilowania rzeczy.
Dave Dopson,
7
Wykonywanie operacji we / wy w module obsługi sygnału to przepis na katastrofę.
Tim Seguine,
6
@TimSeguine: to nieprawda. Musisz tylko upewnić się, że wiesz, co robisz. signal(7)zawiera listę wszystkich funkcji bezpiecznych dla sygnału asynchronicznego, których można używać przy stosunkowo niewielkiej ostrożności. W powyższym przykładzie jest to również całkowicie bezpieczne, ponieważ nic innego w programie nie dotyka stdoutpoza printfwywołaniem w programie obsługi.
stefanct
3
@stefanct To jest przykład zabawki. Praktycznie każdy program nie będący zabawką będzie w pewnym momencie blokować stdout. W przypadku tego modułu obsługi sygnałów najgorsze, co może się prawdopodobnie zdarzyć, to zakleszczenie w segfault, ale może to być wystarczająco złe, jeśli obecnie nie masz mechanizmu do zabijania nieuczciwych procesów w Twoim przypadku użycia.
Tim Seguine
3
zgodnie z 2.4.3 Działania sygnałów , wywołanie printf z modułu obsługi sygnału, który jest wywoływany w wyniku niedozwolonego pośrednictwa, niezależnie od tego, czy program jest wielowątkowy, czy nie, jest po prostu nieokreślonym okresem zachowania .
Julien Villemure-Fréchette
9

Aby zapewnić przenośność, prawdopodobnie należy użyć std::signal standardowej biblioteki C ++, ale istnieje wiele ograniczeń co do tego, co może zrobić program obsługi sygnału. Niestety, nie jest możliwe przechwycenie SIGSEGV z poziomu programu C ++ bez wprowadzenia niezdefiniowanego zachowania, ponieważ specyfikacja mówi:

  1. jest niezdefiniowanym zachowaniem wywoływanie dowolnej funkcji bibliotecznej z modułu obsługi innej niż bardzo wąski podzbiór standardowych funkcji bibliotecznych (abort , exitniektóre funkcje atomowe, zainstaluj aktualną obsługi sygnału, memcpy, memmove, cechy typu, `std :: ruch , std::forward, a niektóre więcej ).
  2. jest to niezdefiniowane zachowanie, jeśli program obsługi używa throwwyrażenia.
  3. jest to niezdefiniowane zachowanie, jeśli funkcja obsługi zwraca podczas obsługi SIGFPE, SIGILL, SIGSEGV

Dowodzi to, że nie da się złapać SIGSEGV z poziomu programu używającego ściśle standardowego i przenośnego C ++. SIGSEGV jest nadal przechwytywany przez system operacyjny i zwykle jest zgłaszany do procesu nadrzędnego, gdy wywoływana jest funkcja rodziny oczekiwania .

Prawdopodobnie napotkasz podobne problemy używając sygnału POSIX, ponieważ istnieje klauzula, która mówi w 2.4.3 Działania sygnału :

Zachowanie procesu jest niezdefiniowane po powrocie normalnie z funkcji przechwytującej sygnał dla sygnału SIGBUS, SIGFPE, SIGILL lub SIGSEGV, który nie został wygenerowany przez kill(),sigqueue() lub raise().

Słowo o longjump s. Zakładając, że używamy sygnałów POSIX, użycie longjumpdo symulacji rozwijania stosu nie pomoże:

Chociaż longjmp()jest funkcją bezpieczną dla sygnału asynchronicznego, jeśli jest wywoływana z programu obsługi sygnału, który przerwał funkcję bezpieczną dla sygnału innego niż asynchroniczny lub równoważną (taką jak przetwarzanie równoważneexit() wykonania po powrocie z początkowego wywołania do main()), zachowanie każdego kolejnego wywołania funkcji bezpiecznej dla sygnału asynchronicznego lub równoważnej jest niezdefiniowane.

Oznacza to, że kontynuacja wywołana przez wywołanie longjump nie może niezawodnie wywołać zwykle użytecznej funkcji bibliotecznej, takiej jak printf, malloclubexit lub zwrot od głównej bez wywołania niezdefiniowanej zachowanie. W związku z tym kontynuacja może wykonywać tylko ograniczone operacje i może zakończyć się tylko przez jakiś nieprawidłowy mechanizm zakończenia.

Krótko mówiąc, przechwycenie SIGSEGV i wznowienie wykonywania programu na urządzeniu przenośnym jest prawdopodobnie niewykonalne bez wprowadzenia UB. Nawet jeśli pracujesz na platformie Windows, dla której masz dostęp do strukturalnej obsługi wyjątków, warto wspomnieć, że MSDN sugeruje, aby nigdy nie próbować obsługiwać wyjątków sprzętowych: Wyjątki sprzętowe

Julien Villemure-Fréchette
źródło
SIGSEGV nie jest jednak wyjątkiem sprzętowym. Zawsze można użyć architektury rodzic-dziecko, w której rodzic jest w stanie wykryć przypadek dziecka, które zostało zabite przez jądro i użyć IPC do udostępnienia odpowiedniego stanu programu w celu wznowienia od miejsca, w którym go opuściliśmy. Uważam, że nowoczesne przeglądarki można postrzegać w ten sposób, ponieważ używają one mechanizmów IPC do komunikacji z tym jednym procesem na karcie przeglądarki. Oczywiście granica bezpieczeństwa między procesami jest bonusem w scenariuszu przeglądarki.
0xC0000022L
8

Rozwiązanie C ++ znalezione tutaj ( 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
źródło
7
Wiem, że to tylko przykład, którego nie napisałeś, ale wykonanie operacji we / wy w module obsługi sygnału to przepis na katastrofę.
Tim Seguine,
3
@TimSeguine: powtarzać rzeczy, które są w najlepszym razie bardzo mylące, nie jest dobrym pomysłem (por stackoverflow.com/questions/2350489/... )
stefanct
3
@stefanct Środki ostrożności konieczne do bezpiecznego używania printf w programie obsługi sygnału nie są trywialne. Nie ma w tym nic mylącego. To jest przykład zabawki. I nawet w tym przykładzie z zabawką możliwe jest zakleszczenie, jeśli odpowiednio ustawisz czas SIGINT. Zakleszczenia są niebezpieczne właśnie, PONIEWAŻ są rzadkie. Jeśli uważasz, że ta rada wprowadzała w błąd, trzymaj się z daleka od mojego kodu, ponieważ nie ufam ci w promieniu mili.
Tim Seguine
Ponownie, mówiłeś tutaj ogólnie o I / O. Zamiast wskazywać na problem z tym faktycznym przykładem, który jest rzeczywiście zły.
stefanct
1
@stefanct Jeśli chcesz podchwycić i zignorować kontekst instrukcji, to jest twój problem. Kto powiedział, że mówię ogólnie o I / O? Ty. Mam po prostu poważny problem z publikowaniem zabawek odpowiedzi na trudne problemy. Nawet w przypadku korzystania z bezpiecznych funkcji asynchronicznych wciąż jest wiele do przemyślenia, a ta odpowiedź sprawia, że ​​wydaje się to trywialne.
Tim Seguine
5

Czasami chcemy złapać a, SIGSEGVaby dowiedzieć się, czy wskaźnik jest prawidłowy, to znaczy, czy odwołuje się do prawidłowego adresu pamięci. (Lub nawet sprawdź, czy jakaś dowolna wartość może być wskaźnikiem).

Jedną z opcji jest sprawdzenie tego za pomocą isValidPtr()(działało na Androidzie):

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);
}

Inną opcją jest odczytanie atrybutów ochrony pamięci, co jest nieco trudniejsze (działało na Androidzie):

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()jest printf()w dzienniku Androida. FIRST_UNUSED_BIT()jest zdefiniowane tutaj .

PPS Wywoływanie funkcji przydziel () w pętli może nie być dobrym pomysłem - pamięć może nie zostać zwolniona do czasu powrotu funkcji.

18446744073709551615
źródło