Linux'ta segmentasyon hatası nasıl yakalanır?


84

Üçüncü taraf kitaplık temizleme işlemlerinde segmentasyon hatasını yakalamam gerekiyor. Bu bazen programımdan çıkmadan hemen önce oluyor ve bunun gerçek nedenini düzeltemiyorum. Windows programlamada bunu __try - __catch ile yapabilirim. Aynı şeyi yapmanın çapraz platform veya platforma özgü bir yolu var mı? Buna Linux'ta ihtiyacım var, gcc.


Segmentasyon hatası her zaman yakalanması gerçekten zor olabilecek bir hatadan kaynaklanır. Sadece rastgele görünen birini buldum. Her dosyanın 500 milyon veri noktası vardır. Yaklaşık her 10-15 dosyada bu bölümleme hatası görülür. Çoklu iş parçacığı, kilitsiz kuyruk vb. Kullanıyordum. Oldukça karmaşık iş yönetimi. Sonunda o oluşturduğum bir nesne, std :: başka bir veri yapısına taşındı (). Yerel olarak bu nesneyi taşındıktan sonra kullanıyordum. Bazı nedenlerden dolayı, C ++ bunda sorun yok. Ancak segfault bir noktada kesin olarak ortaya çıkacaktır.
Kemin Zhou

Yanıtlar:


80

Linux'ta bunları istisnalar olarak da alabiliriz.

Normalde, programınız bir bölümleme hatası yaptığında, bir SIGSEGVsinyal gönderilir . Bu sinyal için kendi işleyicinizi kurabilir ve sonuçlarını hafifletebilirsiniz. Tabii sen gerçekten emin olmalıdır olabilir durumdan kurtarmak. Sizin durumunuzda, bunun yerine kodunuzda hata ayıklamanız gerektiğini düşünüyorum.

Konuya dön. Yakın zamanda bu tür sinyalleri istisnalara dönüştüren bir kitaplıkla ( kısa kılavuz ) karşılaştım , böylece şu şekilde kod yazabilirsiniz:

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

Yine de kontrol etmedim. X86-64 Gentoo kutumda çalışıyor. Platforma özgü bir arka ucu vardır (gcc'nin java uygulamasından ödünç alınmıştır), bu nedenle birçok platformda çalışabilir. Kutudan çıktığı gibi x86 ve x86-64'ü destekler, ancak gcc kaynaklarında bulunan libjava'dan arka uçlar alabilirsiniz.


16
Sig segfault'u yakalamadan önce iyileşebileceğinizden emin olmak
Henrik Mühe

16
Bir sinyal işleyiciden atmak çok tehlikeli bir şeydir. Çoğu derleyici, yalnızca çağrıların istisnalar üretebileceğini varsayar ve buna göre çözme bilgilerini ayarlar. Java ve C # gibi donanım istisnalarını yazılım istisnalarına dönüştüren diller, her şeyin fırlatabileceğinin farkındadır; C ++ ile durum böyle değildir. GCC ile, en azından -fnon-call-exceptionsçalıştığından emin olmanız gerekir - ve bunun bir performans maliyeti vardır. Ayrıca, istisna desteği olmayan bir işlevden (bir C işlevi gibi) ve daha sonra sızıntı / çökme tehlikesi de vardır.
zneak

1
Zneak'a katılıyorum. Bir sinyal işleyiciden atmayın.
MM.

Kitaplık şimdi github.com/Plaristote/segvcatch içinde , ancak kılavuzu bulamadım veya derleyemedim . ./build_gcc_linux_releasebirkaç hata veriyor.
alfC

Yaşasın! Artık dünyadaki tek Gentoo kullanıcısı olmadığımı biliyorum!
SS Anne

46

İşte C'de nasıl yapılacağına dair bir örnek.

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

9
sizeof (sigaction) ==> sizeof (struct sigaction) veya başka bir şeyi derlerken bir ISO C ++ hatası alırsınız.
Dave Dopson

7
Bir sinyal işleyicide IO yapmak, felaket için bir reçetedir.
Tim Seguine

6
@TimSeguine: Bu doğru değil. Sadece ne yaptığını bildiğinden emin olmalısın. signal(7)nispeten az özenle kullanılabilen tüm eşzamansız sinyal güvenli işlevleri listeler. Programda başka bir şey dokunaklı çünkü Yukarıdaki örnekte de tamamen güvenlidir stdoutama printfişleyicisi çağrı.
stefanct

3
@stefanct Bu bir oyuncak örneğidir. Neredeyse oyuncak olmayan herhangi bir program, bir noktada stdout'ta kilidi tutacaktır. Bu sinyal işleyiciyle, olabilecek en kötü şey segfault üzerindeki bir kilitlenmedir, ancak şu anda kullanım durumunuzda hileli işlemleri sonlandıracak bir mekanizmanız yoksa bu yeterince kötü olabilir.
Tim Seguine

3
2.4.3 Sinyal Eylemlerine göre , programın çok iş parçacıklı olup olmadığına bakılmaksızın, yasadışı bir yönlendirmenin bir sonucu olarak çağrılan bir sinyal işleyicisi içinden printf'in çağrılması, sadece tanımsız bir davranış dönemidir.
Julien Villemure-Fréchette

9

Taşınabilirlik için, muhtemelen std::signalstandart C ++ kitaplığından kullanılması gerekir, ancak bir sinyal işleyicinin yapabilecekleri konusunda birçok kısıtlama vardır. Maalesef, tanımsız davranışa neden olmadan bir C ++ programı içinden bir SIGSEGV yakalamak mümkün değildir, çünkü belirtim:

  1. standart kütüphane fonksiyonları (çok dar alt kümesi dışındaki işleyicisi içinde herhangi kütüphane işlevi çağırmak için tanımsız davranıştır abort, exitbazı atom fonksiyonlar, akım sinyali işleyicisi, yeniden memcpy, memmoveyazın özellikleri, `std :: hamleyi , std::forwardbiraz daha ve ).
  2. işleyicinin bir throwifade kullanması tanımsız bir davranıştır .
  3. İşleyicinin SIGFPE, SIGILL, SIGSEGV'yi işlerken dönmesi tanımsız bir davranıştır

Bu, kesinlikle standart ve taşınabilir C ++ kullanan bir programın içinden SIGSEGV yakalamanın imkansız olduğunu kanıtlıyor . SIGSEGV hala işletim sistemi tarafından yakalanır ve normal olarak bir bekleme aile işlevi çağrıldığında ana sürece rapor edilir .

Muhtemelen POSIX sinyalini kullanırken aynı tür sorunlarla karşılaşacaksınız çünkü 2.4.3 Sinyal Eylemleri'nde şöyle bir cümle var :

Bu tarafından oluşturulan olmayan bir SIGBUS, SIGFPE, SIGILL veya SIGSEGV sinyal için bir sinyal alıcı işlevi ile ilgili normal olarak döner sonra bir işlem davranışı tanımlanmamış kill(), sigqueue()ya da raise().

S hakkında bir kelime longjump. POSIX sinyallerini kullandığımızı varsayarsak, longjumpyığın çözmeyi simüle etmek için kullanmak yardımcı olmaz:

longjmp()Eşzamansız sinyal güvenli bir işlev olmasına rağmen , eşzamansız olmayan sinyal güvenli bir işlevi veya eşdeğerini kesintiye uğratan bir sinyal işleyiciden çağrılırsa ( exit()ilk çağrıdan döndükten sonra gerçekleştirilen işleme eşdeğeri gibi main()), eşzamansız olmayan sinyal güvenli bir işleve veya eşdeğerine yapılan sonraki herhangi bir çağrının davranışı tanımsızdır.

Bu, longjump çağrısı tarafından çağrılan devam etmenin, tanımsız davranışa neden olmadan printf, mallocveya main'den dönme gibi genellikle yararlı kitaplık işlevini güvenilir bir şekilde çağıramayacağı anlamına gelir exit. Bu nedenle, devam ettirme yalnızca sınırlı bir işlem yapabilir ve yalnızca bazı anormal sonlandırma mekanizmalarıyla çıkabilir.

Kısaca söylemek gerekirse, bir SIGSEGV yakalamak ve programı bir taşınabilir bilgisayarda yürütmeye devam etmek , UB'yi tanıtmadan muhtemelen mümkün değildir. Yapılandırılmış istisna işlemeye erişiminizin olduğu bir Windows platformunda çalışıyor olsanız bile, MSDN'nin donanım istisnalarını hiçbir zaman işlememeyi önerdiğini belirtmekte fayda var: Donanım İstisnaları


Yine de SIGSEGV bir donanım istisnası değildir. Her zaman, ebeveynin çekirdek tarafından öldürülen bir çocuk vakasını tespit edebildiği ve kaldığımız yerden devam etmek için ilgili program durumunu paylaşmak için IPC'yi kullanabildiği bir ebeveyn-çocuk mimarisi kullanılabilir. Tarayıcı sekmesi başına tek bir işlemle iletişim kurmak için IPC mekanizmalarını kullandıkları için modern tarayıcıların bu şekilde görülebileceğine inanıyorum. Açıkçası, işlemler arasındaki güvenlik sınırı, tarayıcı senaryosunda bir bonus.
0xC0000022L

8

Burada bulunan C ++ çözümü ( 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);
    }
}

7
Bunun sizin yazmadığınız bir örnek olduğunu biliyorum, ancak bir sinyal işleyicide IO yapmak felaket için bir reçetedir.
Tim Seguine

3
@TimSeguine: En iyi ihtimalle çok yanıltıcı olan şeyleri tekrarlamak iyi bir fikir değil (cf. stackoverflow.com/questions/2350489/… )
stefanct

3
@stefanct printf'i bir sinyal işleyicide güvenli bir şekilde kullanmak için gereken önlemler önemsiz değildir. Bunda yanıltıcı hiçbir şey yok. Bu oyuncak bir örnektir. Ve bu oyuncak örneğinde bile SIGINT'i doğru zamanlamanız durumunda kilitlenmek mümkündür. Deadlocks, ender görüldükleri için kesinlikle tehlikelidir. Bu tavsiyenin yanıltıcı olduğunu düşünüyorsanız, o zaman kodumdan uzak durun, çünkü bir mil içinde size güvenmiyorum.
Tim Seguine

Yine, burada genel olarak I / O hakkında konuşuyordunuz. Bu gerçek örnekle soruna işaret etmek yerine, ki bu gerçekten kötüdür.
stefanct

1
@stefanct İfadenin içeriğini nitpick ve görmezden gelmek istiyorsanız, o zaman bu sizin probleminizdir. Genel olarak I / O hakkında konuştuğumu kim söyledi? Sen. Zor problemlere oyuncak cevaplar gönderen insanlarla ilgili büyük bir problemim var. Eşzamansız güvenli işlevler kullanmanız durumunda bile, üzerinde düşünülmesi gereken çok şey var ve bu yanıt, bunun önemsiz gibi görünmesini sağlıyor.
Tim Seguine

5

Bazen SIGSEGVbir göstericinin geçerli olup olmadığını, yani geçerli bir bellek adresine başvurup başvurmadığını öğrenmek için a'yı yakalamak isteriz . (Ya da bazı rastgele değerlerin bir işaretçi olup olmadığını kontrol edin.)

Bir seçenek kontrol etmektir isValidPtr()(Android'de çalıştı):

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

Başka bir seçenek de, biraz daha zor olan (Android'de çalıştı) bellek koruma özniteliklerini okumaktır:

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(), printf()Android günlüğündedir. buradaFIRST_UNUSED_BIT() tanımlanmıştır .

PPS Bir döngüde alloca () ' yı çağırmak iyi bir fikir olmayabilir - işlev dönene kadar bellek serbest kalmayabilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.