Возвращение к жизни после нарушения сегментации

Можно ли восстановить нормальный поток выполнения программы на C после ошибки Segmentation Fault?

struct A {
    int x;
};
A* a = 0;

a->x = 123; // this is where segmentation violation occurs

// after handling the error I want to get back here:
printf("normal execution");
// the rest of my source code....

Мне нужен механизм, похожий на NullPointerException, который присутствует в Java, C# и т. Д.

Примечание: пожалуйста, не говорите мне, что в C++ есть механизм обработки исключений, потому что я знаю это, не говорите мне, что я должен проверять каждый указатель перед присваиванием и т.д.

Чего я действительно хочу добиться, так это вернуться к нормальному потоку выполнения, как в примере выше. Я знаю, что некоторые действия могут быть предприняты с использованием сигналов POSIX. Как это должно выглядеть? Другие идеи?

13 ответов

Решение
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <signal.h>
#include <stdlib.h>
#include <ucontext.h>

void safe_func(void)
{
    puts("Safe now ?");
    exit(0); //can't return to main, it's where the segfault occured.
}

void
handler (int cause, siginfo_t * info, void *uap)
{
  //For test. Never ever call stdio functions in a signal handler otherwise*/
  printf ("SIGSEGV raised at address %p\n", info->si_addr);
  ucontext_t *context = uap;
  /*On my particular system, compiled with gcc -O2, the offending instruction
  generated for "*f = 16;" is 6 bytes. Lets try to set the instruction
  pointer to the next instruction (general register 14 is EIP, on linux x86) */
  context->uc_mcontext.gregs[14] += 6; 
  //alternativly, try to jump to a "safe place"
  //context->uc_mcontext.gregs[14] = (unsigned int)safe_func;
}

int
main (int argc, char *argv[])
{
  struct sigaction sa;
  sa.sa_sigaction = handler;
  int *f = NULL;
  sigemptyset (&sa.sa_mask);
  sa.sa_flags = SA_SIGINFO;
  if (sigaction (SIGSEGV, &sa, 0)) {
      perror ("sigaction");
      exit(1);
  }
  //cause a segfault
  *f = 16; 
  puts("Still Alive");
  return 0;
}

$ ./a.out
SIGSEGV raised at address (nil)
Still Alive

Я бы побил кого-то битой, если бы увидел что-то подобное в рабочем коде, хотя это уродливый, забавный хак. Вы не будете знать, повредил ли segfault некоторые из ваших данных, у вас не будет нормального способа восстановления, и вы будете знать, что все в порядке, нет портативного способа сделать это. Единственная слегка вменяемая вещь, которую вы можете сделать, это попытаться записать ошибку (использовать write() напрямую, а не любую из функций stdio - они небезопасны) и, возможно, перезапустить программу. В этих случаях вам гораздо лучше написать процесс супервизора, который отслеживает выход дочернего процесса, регистрирует его и запускает новый дочерний процесс.

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

Название сигнала SIGSEGV,

Вам придется использовать sigaction() функция от signal.h заголовок.

В основном это работает следующим образом:

struct sigaction sa1;
struct sigaction sa2;

sa1.sa_handler = your_handler_func;
sa1.sa_flags   = 0;
sigemptyset( &sa1.sa_mask );

sigaction( SIGSEGV, &sa1, &sa2 );

Вот прототип функции-обработчика:

void your_handler_func( int id );

Как видите, вам не нужно возвращаться. Выполнение программы будет продолжено, если вы сами не решите остановить его из обработчика.

"Все вещи допустимы, но не все полезны", - как правило, segfault - игра закончена по уважительной причине... Лучшей идеей, чем выяснить, где было бы сохранение ваших данных (базы данных или, по крайней мере, файла) система) и позволить ему подобрать, где он остановился таким образом. Это значительно повысит надежность данных.

См. Комментарий Р. к ответу МакМейда.

Если продолжить с того, что он сказал (после обработки SIGSEV или, в этом случае, SIGFPE, CPU+OS может вернуть вас в инсин), то у меня есть тест для деления на ноль:

#include <stdio.h>
#include <limits.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>

static jmp_buf  context;

static void sig_handler(int signo)
{
    /* XXX: don't do this, not reentrant */
    printf("Got SIGFPE\n");

    /* avoid infinite loop */
    longjmp(context, 1);
}

int main()
{
    int a;
    struct sigaction sa;

    memset(&sa, 0, sizeof(struct sigaction));
    sa.sa_handler = sig_handler;
    sa.sa_flags = SA_RESTART;
    sigaction(SIGFPE, &sa, NULL);

    if (setjmp(context)) {
            /* If this one was on setjmp's block,
             * it would need to be volatile, to
             * make sure the compiler reloads it.
             */
            sigset_t ss;

            /* Make sure to unblock SIGFPE, according to POSIX it
             * gets blocked when calling its signal handler.
             * sigsetjmp()/siglongjmp would make this unnecessary.
             */
            sigemptyset(&ss);
            sigaddset(&ss, SIGFPE);
            sigprocmask(SIG_UNBLOCK, &ss, NULL);

            goto skip;
    }

    a = 10 / 0;
skip:
    printf("Exiting\n");

    return 0;
}

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

Рассмотрим некоторые из возможных причин ошибки сегментации:

  • Вы забыли назначить правильное значение указателю
  • указатель был перезаписан, возможно, потому что вы обращаетесь к памяти кучи, которую вы освободили
  • ошибка повредила кучу
  • ошибка повредила стек
  • злонамеренная третья сторона пытается использовать уязвимость переполнения буфера
  • malloc возвратил ноль, потому что у вас недостаточно памяти

Только в первом случае можно ожидать каких-либо разумных ожиданий

Если у вас есть указатель, который вы хотите разыменовать, но он может быть на законном основании нулевым, вы должны проверить его перед попыткой разыменования. Я знаю, что вы не хотите, чтобы я вам это говорил, но это правильный ответ, такой жесткий.

Изменить: вот пример, чтобы показать, почему вы определенно не хотите продолжать следующую инструкцию после разыменования нулевого указателя:

void foobarMyProcess(struct SomeStruct* structPtr)
{
    char* aBuffer = structPtr->aBigBufferWithLotsOfSpace; // if structPtr is NULL, will SIGSEGV
    //
    // if you SIGSEGV and come back to here, at this point aBuffer contains whatever garbage was in memory at the point
    // where the stack frame was created
    //
    strcpy(aBuffer, "Some longish string");  // You've just written the string to some random location in your address space
                                             // good luck with that!

}

Нет никакого значимого способа восстановления после SIGSEGV, если вы не знаете ТОЧНО, что вызвало его, и нет способа сделать это в стандартном C. Это может быть (возможно) в инструментальной среде, такой как C-VM (?). То же самое верно для всех сигналов ошибки программы; если вы попытаетесь заблокировать / игнорировать их или установить обработчики, которые возвращаются нормально, ваша программа, вероятно, будет ужасно прерваться, если они не будут сгенерированы raise или же kill,

Просто сделайте себе одолжение и примите во внимание случаи ошибок.

Вызовите это, и когда произойдет segfault, ваш код выполнит segv_handler и затем вернется к тому месту, где он был.

void segv_handler(int)
{
  // Do what you want here
}

signal(SIGSEGV, segv_handler);

Вы можете использовать функцию SetUnhandledExceptionFilter() (в Windows), но даже для того, чтобы пропустить "недопустимую" инструкцию, вам нужно будет иметь возможность декодировать некоторые коды операций на ассемблере. И, как сказал glowcoder, даже если он "закомментирует" во время выполнения инструкции, которые генерируют segfaults, что останется от исходной логики программы (если ее можно так назвать)? Все возможно, но это не значит, что это нужно делать.

Лучшее решение состоит в том, чтобы входить в каждый небезопасный доступ следующим образом:

#include <iostream>
#include <signal.h>
#include <setjmp.h>
static jmp_buf buf;
int counter = 0;
void signal_handler(int)
{
     longjmp(buf,0);
}
int main()
{
    signal(SIGSEGV,signal_handler);
    setjmp(buf);
    if(counter++ == 0){ // if we did'nt try before
    *(int*)(0x1215) = 10;  // access an other process's memory
    }
    std::cout<<"i am alive !!"<<std::endl; // we will get into here in any case
    system("pause");
 return 0;   
}

ваша программа никогда не будет зависать почти во всех ОС

Я вижу в случае восстановления после нарушения сегментации, если ваши события обработки в цикле и одно из этих событий вызывают нарушение сегментации, вам нужно только пропустить это событие и продолжить обработку оставшихся событий. На мой взгляд, нарушение сегментации во многом совпадает с исключениями NullPointerException в Java. Да, состояние будет непоследовательным и неизвестным после любого из них, однако в некоторых случаях вы хотели бы справиться с ситуацией и продолжить. Например, в торговле Algo вы бы приостановили выполнение ордера и позволили трейдеру взять на себя управление вручную, не нарушая всю систему и не разрушая все остальные ордера.

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

Что вы можете сделать, это запустить функции в новом процессе. Если этот процесс завершается с кодом возврата, который указывает SIGSEGV, вы знаете, что это не удалось.

Вы также можете переписать функции самостоятельно.

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

Это руководство дает вам четкое представление о том, как писать обработчики сигналов.

A signal handler is just a function that you compile together with the rest
of the program. Instead of directly invoking the function, you use signal 
or sigaction to tell the operating system to call it when a signal arrives.
This is known as establishing the handler.

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

Обработчики сигналов в целом подразделяются на категории буксируемых

  1. Вы можете сделать так, чтобы функция-обработчик заметила, что сигнал поступил путем настройки некоторых глобальных структур данных, а затем нормально вернулся.
  2. Вы можете сделать так, чтобы функция обработчика завершила программу или передала управление в точку, в которой оно может восстановиться после ситуации, вызвавшей сигнал.

SIGSEGV попадает под сигналы ошибок программы

Другие вопросы по тегам