Как отловить ошибку сегментации в Linux?

84

Мне нужно отловить ошибку сегментации в операциях очистки сторонней библиотеки. Иногда это происходит непосредственно перед выходом из моей программы, и я не могу исправить настоящую причину этого. В программировании под Windows я мог сделать это с помощью __try - __catch. Есть ли кроссплатформенный или специфичный для платформы способ сделать то же самое? Мне это нужно в Linux, gcc.

Алекс Ф
источник
Ошибка сегментации всегда вызвана ошибкой, отловить которую очень сложно. Я просто нахожу тот, который появляется случайно. Каждый файл содержит 500 миллионов точек данных. Эта ошибка сегментации появляется примерно через каждые 10-15 файлов. Я использовал многопоточность, очередь без блокировок и т. Д. Довольно сложное управление заданиями. В конце концов, это объект, который я создал, std :: move () в другую структуру данных. Локально я использовал этот объект после переезда. По какой-то причине C ++ с этим согласен. Но в какой-то момент segfault обязательно появится.
Кемин Чжоу

Ответы:

80

В Linux они тоже могут быть исключениями.

Обычно, когда ваша программа выполняет ошибку сегментации, ей посылается SIGSEGVсигнал. Вы можете настроить свой собственный обработчик для этого сигнала и смягчить последствия. Конечно, вы действительно должны быть уверены, что сможете оправиться от ситуации. В вашем случае, я думаю, вам следует вместо этого отладить свой код.

Вернемся к теме. Недавно я столкнулся с библиотекой ( кратким руководством ), которая преобразует такие сигналы в исключения, поэтому вы можете написать такой код:

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

Но не проверял. Работает на моем компьютере Gentoo x86-64. Он имеет платформенно-зависимый бэкэнд (заимствованный из java-реализации gcc), поэтому он может работать на многих платформах. Он просто поддерживает x86 и x86-64 из коробки, но вы можете получить бэкэнд из libjava, который находится в исходных кодах gcc.

П Швед
источник
16
+1 для уверенности, что вы сможете восстановиться, прежде чем поймаете sig segfault
Хенрик Мюэ
16
Выбрасывание из обработчика сигнала - очень опасная вещь. Большинство компиляторов предполагают, что только вызовы могут генерировать исключения, и соответствующим образом настраивают информацию о раскручивании. Языки, которые преобразуют аппаратные исключения в программные исключения, такие как Java и C #, знают, что все может вызвать ошибку; в C ++ дело обстоит иначе. Используя GCC, вы, по крайней мере, должны -fnon-call-exceptionsубедиться, что он работает, а это требует снижения производительности. Также существует опасность того, что вы выбрасываете из функции без поддержки исключений (например, функции C) и утечки / сбоя позже.
знеак
1
Я согласен со знеаком. Не бросайте из обработчика сигналов.
ММ.
Библиотека сейчас находится на github.com/Plaristote/segvcatch , но мне не удалось найти руководство или скомпилировать его. ./build_gcc_linux_releaseдает несколько ошибок.
alfC
Ура! Теперь я знаю, что я не единственный пользователь Gentoo в мире!
СС Энн
46

Вот пример того, как это сделать на 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
источник
9
sizeof (sigaction) ==> sizeof (struct sigaction), иначе вы получите ошибку ISO C ++ при компиляции объекта.
Дэйв Допсон
7
Выполнение операций ввода-вывода в обработчике сигналов - это верный путь к катастрофе.
Тим Сегин
6
@TimSeguine: это неправда. Вам просто нужно убедиться, что вы знаете, что делаете. signal(7)перечисляет все функции, безопасные для асинхронных сигналов, которые можно использовать с относительно небольшой осторожностью. В приведенном выше примере это также полностью безопасно, потому что ничего другого в программе не касается, stdoutкроме printfвызова в обработчике.
stefanct
3
@stefanct Это игрушечный пример. Практически любая программа, не являющаяся игрушечной, в какой-то момент будет блокировать стандартный вывод. С этим обработчиком сигналов худшее, что может произойти, - это тупик на segfault, но этого может быть достаточно, если у вас в настоящее время нет механизма для уничтожения мошеннических процессов в вашем варианте использования.
Тим Сегин
3
в соответствии с 2.4.3 Действия с сигналами, вызов printf из обработчика сигнала, который вызывается в результате недопустимого косвенного обращения, независимо от того, является ли программа многопоточной или нет, является просто периодом неопределенного поведения .
Julien Villemure-Fréchette
9

Для переносимости, вероятно, следует использовать std::signalстандартную библиотеку C ++, но есть много ограничений на то, что может делать обработчик сигналов. К сожалению, невозможно поймать SIGSEGV из программы на C ++ без введения неопределенного поведения, потому что в спецификации сказано:

  1. это неопределенное поведение для вызова любой библиотечной функции из обработчика, кроме очень узкого подмножества стандартных библиотечных функций ( abort, exit, некоторые атомарные функций, переустановите текущий обработчик сигнала memcpy, memmove, черты типа, `зОго :: двигаться , std::forward, и еще немного ).
  2. это неопределенное поведение, если обработчик использует throwвыражение.
  3. это неопределенное поведение, если обработчик возвращается при обработке SIGFPE, SIGILL, SIGSEGV

Это доказывает, что невозможно поймать SIGSEGV из программы, использующей строго стандартный и переносимый C ++. SIGSEGV по-прежнему перехватывается операционной системой и обычно передается родительскому процессу при вызове функции семейства ожидания .

Вы, вероятно, столкнетесь с такими же проблемами при использовании сигнала POSIX, потому что в 2.4.3 Signal Actions есть пункт, который гласит :

Поведение процесса является неопределенным после того, как он возвращается , как правило из функции сигнала выделяющихся для SIGBUS, SIGFPE, SIGILL, или сигнала SIGSEGV , который не был генерируемыми kill(), sigqueue()или raise().

Несколько слов о longjumpс. Предполагая, что мы используем сигналы POSIX, использование longjumpдля имитации раскрутки стека не поможет:

Хотя longjmp()это функция, безопасная для асинхронных сигналов, если она вызывается из обработчика сигналов, который прервал функцию, не являющуюся безопасным для асинхронных сигналов, или эквивалентную (например, обработку, эквивалентную exit()выполненной после возврата из первоначального вызова в main()), поведение любого последующего вызова функции, не защищенной от асинхронного сигнала, или ее эквивалента не определено.

Это означает, что продолжение, вызванное вызовом longjump, не может надежно вызвать обычно полезную библиотечную функцию, такую ​​как printf, mallocили, exitили возврат из main, не вызывая неопределенного поведения. Таким образом, продолжение может выполнять только ограниченные операции и может выходить только через какой-то ненормальный механизм завершения.

Короче говоря, перехват SIGSEGV и возобновление выполнения программы на переносном компьютере, вероятно, невозможно без введения UB. Даже если вы работаете на платформе Windows, для которой у вас есть доступ к структурированной обработке исключений, стоит упомянуть, что MSDN предлагает никогда не пытаться обрабатывать аппаратные исключения: Аппаратные исключения

Жюльен Вильмюр-Фрешет
источник
Однако SIGSEGV вряд ли является исключением из аппаратного обеспечения. Всегда можно использовать родительско-дочернюю архитектуру, в которой родитель может обнаружить случай, когда дочерний элемент был убит ядром, и использовать IPC для совместного использования соответствующего состояния программы, чтобы возобновить работу с того места, откуда мы ушли. Я считаю, что современные браузеры можно увидеть именно так, поскольку они используют механизмы IPC для связи с этим процессом на каждой вкладке браузера. Очевидно, что граница безопасности между процессами - это бонус в сценарии браузера.
0xC0000022L 06
8

Решение C ++ найдено здесь ( 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
источник
7
Я знаю, что это просто пример, который вы не писали, но выполнение ввода-вывода в обработчике сигналов - это верный путь к катастрофе.
Тим Сегин
3
@TimSeguine: повторять материал , который в лучшем случае очень заблуждение , это не очень хорошая идея (см stackoverflow.com/questions/2350489/... )
stefanct
3
@stefanct Меры предосторожности, необходимые для безопасного использования printf в обработчике сигналов, нетривиальны. В этом нет ничего вводящего в заблуждение. Это игрушечный пример. И даже в этом игрушечном примере возможно зайти в тупик, если правильно рассчитать время SIGINT. Тупики опасны именно ПОТОМУ ЧТО они редки. Если вы думаете, что этот совет вводит в заблуждение, держитесь подальше от моего кода, потому что я не доверяю вам ни в одной миле от него.
Тим Сегин
Опять же, вы говорили здесь о вводе-выводе в целом. Вместо того, чтобы указывать на проблему с этим фактическим примером, который действительно плохой.
stefanct
1
@stefanct Если вы хотите придираться и игнорировать контекст оператора, то это ваша проблема. Кто сказал, что я говорю о вводе-выводе в целом? Вы. Просто у меня большая проблема с людьми, которые публикуют игрушечные ответы на сложные проблемы. Даже если вы используете асинхронные безопасные функции, есть над чем подумать, и этот ответ делает его тривиальным.
Тим Сегин,
5

Иногда нам нужно поймать a, SIGSEGVчтобы узнать, действителен ли указатель, то есть ссылается ли он на действительный адрес памяти. (Или даже проверьте, может ли какое-то произвольное значение быть указателем.)

Один из вариантов - проверить это isValidPtr()(работает на 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);
}

Другой вариант - прочитать атрибуты защиты памяти, что немного сложнее (работает на 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()находится printf()в журнале Android. FIRST_UNUSED_BIT()определяется здесь .

PPS Возможно, не стоит вызывать alloca () в цикле - память может не освобождаться, пока функция не вернется.

18446744073709551615
источник