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

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


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


Ответы (5)


В 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.

person P Shved    schedule 28.02.2010
comment
+1 за убедитесь, что вы сможете восстановиться, прежде чем поймаете sig segfault - person Henrik Mühe; 02.02.2014
comment
Выбрасывание из обработчика сигнала - очень опасное занятие. Большинство компиляторов предполагают, что только вызовы могут генерировать исключения, и соответствующим образом настраивают информацию раскрутки. Языки, которые преобразуют аппаратные исключения в программные исключения, такие как Java и C #, знают, что все может вызвать ошибку; в C ++ дело обстоит иначе. С GCC вам, по крайней мере, нужно -fnon-call-exceptions, чтобы убедиться, что он работает, и это связано с потерей производительности. Также существует опасность того, что вы выбрасываете из функции без поддержки исключений (например, функции C) и утечки / сбоя позже. - person zneak; 04.06.2015
comment
Я согласен со знеаком. Не бросайте из обработчика сигналов. - person MM.; 12.08.2015
comment
Библиотека сейчас находится в github.com/Plaristote/segvcatch, но мне не удалось найти руководство или скомпилируйте это. ./build_gcc_linux_release выдает несколько ошибок. - person alfC; 23.12.2016
comment
Ура! Теперь я знаю, что я не единственный пользователь Gentoo в мире! - person S.S. Anne; 02.09.2019

Вот пример того, как это сделать на 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;
}
person JayM    schedule 12.03.2010
comment
sizeof (sigaction) == ›sizeof (struct sigaction), иначе вы получите ошибку ISO C ++ при компиляции объекта. - person Dave Dopson; 20.12.2011
comment
Могу ли я получить трассировку стека при его сигнале? - person daisy; 09.12.2013
comment
Выполнение операций ввода-вывода в обработчике сигналов - верный путь к катастрофе. - person Tim Seguine; 06.11.2016
comment
Как мы могли бы объединить его с setjmp / longjmp для имитации блока catch в стиле C ++? Я попробовал longjmp'ing из обработчика, и это не сработало. - person ogurets; 12.05.2017
comment
@TimSeguine: это неправда. Вам просто нужно убедиться, что вы знаете, что делаете. signal(7) перечисляет все безопасные для асинхронных сигналов функции, которые можно использовать с относительно небольшой осторожностью. В приведенном выше примере это также полностью безопасно, потому что ничто другое в программе не касается stdout, кроме вызова printf в обработчике. - person stefanct; 17.11.2017
comment
@stefanct Это игрушечный пример. Практически любая неигровая программа в какой-то момент будет блокировать стандартный вывод. С этим обработчиком сигналов худшее, что, вероятно, может произойти, - это тупик на segfault, но этого может быть достаточно, если у вас в настоящее время нет механизма для уничтожения мошеннических процессов в вашем случае использования. - person Tim Seguine; 17.11.2017
comment
Вы говорили о вводе-выводе в целом; без ограничений; без каких-либо объяснений. Нет ничего плохого или опасного в создании нового файла (потока) и выводе в него информации, равно как и запись в STDOUT_FILENO напрямую. Таким образом, ваш комментарий вводит в заблуждение, и я его критиковал. - person stefanct; 24.11.2017
comment
@stefanct Вы игнорируете контекст. Я ничего не сказал об общем вводе-выводе. Но раз уж вы это подняли: чтение и запись имеют проблемы с синхронизацией. Их использование в асинхронном коде нетривиально, и если исходить из ошибочного игрушечного примера, который в основном говорит: «Посмотрите, как это просто», это действительно рецепт катастрофы. Я не понимаю, как вы ожидаете, что кто-то волшебным образом перейдет от кода обработки сигналов карго-культа к знанию предметной области и будет принимать во внимание каждую мелочь. Я хотел передать сообщение НЕ КОПИРУЙТЕ ЭТОТ ПРИМЕР. Если этого не произошло, то это мое единственное сожаление. - person Tim Seguine; 26.11.2017
comment
согласно 2.4.3 Signal Actions, вызов printf изнутри обработчик сигнала, который вызывается в результате недопустимого косвенного обращения, независимо от того, является ли программа многопоточной или нет, является просто периодом undefined behavior. - person Julien Villemure-Fréchette; 23.11.2018
comment
LOL, катастрофа уже случилась. Вы также можете попытаться рассказать пользователю, что произошло. Кроме того, 99% ошибок сегментов - это попытка разыменовать указатель NULL, поэтому распечатка значений соответствующих указателей (если ваш обработчик локализован) большую часть времени будет работать, чтобы идентифицировать конкретную переменную, вызывающую проблему. . Распечатка этого является ключевым моментом, потому что после выпуска программного обеспечения у вас не будет доступа к машине пользователя, поэтому вы должны полагаться на то, что они вам говорят. Если они говорят вам, что получили сообщение о том, что указатель XYZ был нулевым, это ключевая информация. - person Tyler Durden; 20.06.2021

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

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

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

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

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

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

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

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

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

И наконец, что не менее важно, возникнет ли какой-либо SIGSEGV при разыменовании указателя с нулевым значением (или указателя с недопустимым значением), это не является требованием стандарта. Поскольку косвенное обращение через указатель с нулевым значением или любой указатель с недопустимым значением является неопределенным поведением < / strong>, что означает, что компилятор предполагает, что ваш код никогда не попытается сделать это во время выполнения, компилятор может произвести преобразование кода, которое исключит такое неопределенное поведение. Например, из cppreference,

int foo(int* p) {
    int x = *p;
    if(!p)
        return x; // Either UB above or this branch is never taken
    else
        return 0;
}
 
int main() {
    int* p = nullptr;
    std::cout << foo(p);
}

Здесь истинный путь if может быть полностью опущен компилятором в качестве оптимизации; только часть else могла быть сохранена. В противном случае компилятор заключает, что foo() никогда не получит указатель с нулевым значением во время выполнения, поскольку это приведет к неопределенному поведению. Вызывая его с указателем с нулевым значением, вы можете наблюдать значение 0, выводимое на стандартный вывод, и отсутствие сбоя, вы можете наблюдать сбой с SIGSEG, на самом деле вы могли наблюдать что угодно, поскольку к программам, которые не свободны от undefined, не предъявляются разумные требования. поведение.

person Julien Villemure-Fréchette    schedule 22.11.2018
comment
Однако SIGSEGV вряд ли является исключением из аппаратного обеспечения. Всегда можно использовать родительско-дочернюю архитектуру, в которой родитель может обнаружить случай, когда дочерний элемент был убит ядром, и использовать IPC для совместного использования соответствующего состояния программы, чтобы возобновить работу с того места, откуда мы ушли. Я считаю, что современные браузеры можно рассматривать именно так, поскольку они используют механизмы IPC для связи с этим процессом на каждой вкладке браузера. Очевидно, что граница безопасности между процессами - это бонус в сценарии браузера. - person 0xC0000022L; 06.11.2020

Здесь можно найти решение 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);
    }
}
person revo    schedule 23.12.2012
comment
Я знаю, что это всего лишь пример, который вы не писали, но выполнение ввода-вывода в обработчике сигналов - это верный путь к катастрофе. - person Tim Seguine; 06.11.2016
comment
@stefanct Меры предосторожности, необходимые для безопасного использования printf в обработчике сигналов, нетривиальны. В этом нет ничего вводящего в заблуждение. Это игрушечный пример. И даже в этом игрушечном примере возможно зайти в тупик, если правильно рассчитать SIGINT. Тупики опасны именно ПОТОМУ ЧТО они редки. Если вы думаете, что этот совет вводит в заблуждение, держитесь подальше от моего кода, потому что я не доверяю вам ни в одной миле от него. - person Tim Seguine; 17.11.2017
comment
Опять же, здесь вы говорили о вводе-выводе в целом. Вместо того, чтобы указывать на проблему с этим фактическим примером, который действительно является плохим. - person stefanct; 24.11.2017
comment
@stefanct Если вы хотите придираться и игнорировать контекст оператора, то это ваша проблема. Кто сказал, что я говорю о вводе-выводе в целом? Ты. Просто у меня большая проблема с людьми, которые публикуют игрушечные ответы на сложные проблемы. Даже если вы используете асинхронные безопасные функции, есть над чем подумать, и этот ответ кажется тривиальным. - person Tim Seguine; 26.11.2017
comment
Только для обсуждения. Скажем, я пытаюсь написать реентерабельную функцию для обработки данных, возможно, используя сторонний код, который выдавал редкие случайные ошибки SIGSEGV (никогда не используйте такую ​​библиотеку в первую очередь, но спешите выполнить работу). Поскольку это редко встречающееся событие, я могу использовать стратегию, чтобы попробовать несколько раз и надеяться, что он пройдет этот раздел (обычно это работает). В таком случае (не надежное решение) я хочу обработать ошибку сегментации и перезапустить свою функцию с того места, где она не удалась (скажем, элемент вектора 9999). Вероятно, это невозможно, потому что вам нужна память для чтения? - person Kemin Zhou; 22.11.2018

Иногда нам нужно поймать 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 () в цикле - память может не освобождаться, пока функция не вернется.

person 18446744073709551615    schedule 06.06.2014