Обработка Segmentation Fault в C++

Вводная


C++ является «небезопасным» («unmanaged») языком, поэтому программы могут «вылетать» — аварийно завершать работу без сохранения данных пользователя, сообщения об ошибке и т.п. — стоит только, например, залезть в не инициализированную память. Например:
void fall()
{
  char * s = "short_text";
  sprintf(s,"This is very long text");
}

или
void fall()
{
  int * pointer = NULL;
  *pointer = 13;
}


Всем было бы лучше, если бы мы могли «отловить» падение программы — точно так же, как в java ловим исключения — и выполнить хоть что-то перед тем, как программа упадет (сохранить документ пользователя, вывести диалог с сообщением об ошибке и т.п.)

Общего решения задача не имеет, так как C++ не имеет собственной модели обработки исключений, связанных с работой с памятью. Тем не менее, мы рассмотрим два способа, использующих особенности операционной системы, вызвавшей исключение.



Способ 1: SEH


Если Вы используете OS Windows в качестве целевой ОС и Visual C++ в качестве компилятора, то Вы можете использовать Structured Exception Handling — расширение языка С++ от Microsoft, позволяющее отлавливать любые исключения, происходящие в программе.

Общий синтаксис обработки исключений выглядит следующим образом:

__try
{
  segfault1();
}
__except( condition1 )
{
  // обработка исключения, если condition1 == EXCEPTION_EXECUTE_HANDLER.
  // в condition1 может (должен) быть вызов метода, проверяющего 
  //    тип исключения, и возвращающего EXCEPTION_EXECUTE_HANDLER 
  //    если тип исключения соответствует тому, что мы хотим обработать
}
__except( condition2 )
{
  // еще один обработчик
}
__finally
{
  // то, что выполнится если ни один из обработчиков не почешется
}


Вот «работающий пример» — «скопируй и вставь в Visual Studio»
#include <stdio.h>
#include <windows.h>
#include <excpt.h>

int memento() // обработка Segfault
{
	MessageBoxA(NULL,"Memento Mori","Exception catched!",NULL);
	return 0;
}

void fall() // генерация segfault
{
	  int* p = 0x00000000;   
	  *p = 13;
}

int main(int argc, char *argv[])
{
	__try
	{
		fall();
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		memento();
	}
}


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

Данная методика, при всей ее привлекательности, имеет ряд минусов:

  • Один компилятор. Одна ОС. Не «чистый С++». Если Вы хотите работать без средств MS — Вы не сможете использовать эту методику
  • Один поток — одна таблица. Если Вы напишете конструкцию из __try… __except, внутри __try запустите другой поток и, не выходя из __try второй поток вызовет segfault, то… ничего не произойдет, программа упадет «как обычно». Потому, что на каждый поток нужно писать отдельный обработчик SEH.


Минусов оказалось настолько много, что приходится искать второе решение.

Способ 2: POSIX — сигналы



Способ рассчитан на то, что в момент падения программа получает POSIX-сообщение SIGSEGV. Это безусловно так во всех UNIX-системах, но это фактически так (хотя никто не гарантировал, windows — не posix-совместима) и в windows тоже.

Методика простая — мы должны написать обработчик сообщения SIGSEGV, в котором программа совершит «прощальные действия» и, наконец, упадет:
void posix_death_signal(int signum)
{
	memento(); // прощальные действия
        signal(signum, SIG_DFL); // перепосылка сигнала
	exit(3); //выход из программы. Если не сделать этого, то обработчик будет вызываться бесконечно.
}

после чего мы должны зарегистрировать этот обработчик:
signal(SIGSEGV, posix_death_signal);


Вот готовый пример:
#include <stdio.h>
#include <stdio.h>
#include <windows.h>
#include <stdlib.h>
#include <signal.h>


int memento()
{
	int a=0;
	MessageBoxA(NULL,"Memento mori","POSIX Signal",NULL);
	return 0;
}
void fall()
{
	  int* p = 0x00000000; 
	  *p = 13;
}
void posix_death_signal(int signum)
{
	memento();
	signal(signum, SIG_DFL);
	exit(3);
}


int main(int argc, char *argv[])
{
	signal(SIGSEGV, posix_death_signal);
	fall();
}


В отличие от SEH, это работает всегда: решение «многопоточное» (вы можете уронить программу в любом потоке, обработчик запустится в любом случае) и «кроссплатформенное» — работает под любым компилятором, и под любой POSIX-совместимой ОС.
Share post

Similar posts

Comments 59

    +2
    > и под любой ОС.
    Под любой POSIX-ос :)
      +2
      > Под любой POSIX-ос :)
      Вне всяких сомнений. Сейчас попробую поправить топик
        +2
        У меня в винде обычный C++ try {} catch(...) {} ловит SIGSEGV
          0
          это потому, что «на самом деле» даже обычные try и catch являются обработчиками SEH
            0
            Вполне. Кстати, как с этим в POSIX? :)
            Минус только в том что приходится писать catch(...), т.е. SIGSEGV не отличить от SIGFPE. либо надо юзать какие-то хитрости, просто которые я не-знаю/не-помню.
      +1
      Я бы еще рассказал про особенности SEH под x64, там весьма забавные вещи.
      Ну и про VEH можно упомянуть.
        +1
        @mark_ablov, прошу вас!

        Я-то сам не слишком много работал с SEH, так что не знаю, что там с x64.

        Расскажите — вкратце?
          +1
          В 2х словах не описать, но на статью не тянет, да и вроде есть несколько статей уже по этой тематике.
            +1
            Насколько я знаю, в 64х битном коде можно ловить SEH исключения, сгенерированные в 32х битном, но нельзя их пробрасывать выше по стеку, если там 32х битный код. Это проявляется, например, в том, что в 32х битных оконных приложениях исключения, выброшенные в обработчиках оконных сообщений, т.е. из стека DispatchMessage, просто проглатываются операционной системой. Т.е. если в приложении, запущенном под Windows XP, при клике по кнопке вываливается сообщение об ошибке, то в Windows 7 x64 не произойдет ничего. И потенциально приложение может перейти в несогласованное состояние, если код был написан в расчете на перехват исключений. Ситуация редкая, но неприятная.
          +10
          В случае gcc под x86 имеются замечательные встроенные функции backtrace и backtrace_symbols, которые позволяют получить читабельный бектрейс в конкретной точке кода, в том числе и в обработчике SIGSEGV.
          Жаль, что в gcc под ARM такого нет и приходится извращаться с ассемблером…
            –1
            Да что вы говорите.
            gcc version 4.3.2 (crosstool-NG-1.9.3)

            #include <stdio.h>
            #include <execinfo.h>
            #include <inttypes.h>

            void print_backtrace(void) {
            void* buf[64];
            int size = backtrace(buf, 64);
            printf("backtrace(%d):\n", size);
            for (int i = 0; i < size; ++i)
            printf(" %lx\n", (unsigned long)buf[i]);
            }

            void f(void) {
            print_backtrace();
            }

            int main(void) {
            f();
            return 0;
            }


            arm-unknown-linux-gnueabi-g++ zz.cc -o zz -O0 -march=armv7-a

            backtrace(4):
            84ec
            8574
            8588
            4027cc3a
              +1
              хм, лишних переносов строк в предпросмотре не было
                +1
                читаем ман: The symbol names may be unavailable without the use of special linker options. For systems using the GNU linker, it is necessary to use the -rdynamic linker option.
                  0
                  Вы меня неправильно поняли. Я отвечал на утверждение что в gcc для ARM backtrace() не работает.

                  Работает. И в весьма древнем GCC.

                  А символов нет потому что я backtrace_symbols() не вызываю :) Если вызвать — появятся. Конечно, придется добавить что-то вроде -rdynamic, чтобы нужные символы оказались в динамической таблице.
            +3
            Если уж речь зашла даже о реализации __try{}__except блоков, то почему не упомянули стандартный интерфейс SetUnhandledExceptionFilter?
            Он как раз ближе к обработке в стиле posix, которую Вы хвалите и реализуется системой, а не компилятором.
              +1
              Тем, кому интересна информация про SEH ( ну и чтобы не было «не удалось заставить заработать» ) советую почитать:
              wasm.ru/article.php?article=Win32SEHPietrek1 ч1
              wasm.ru/article.php?article=Win32SEHPietrek2 ч2
              wasm.ru/article.php?article=Win32SEHPietrek3 ч3
                +2
                Прикольно!

                Подумалось — а вдруг чудо возможно, и, установив брейкпоинт внутри «int memento()», удастся увидеть стек вызовов? При этом указывающий на причину, конечно же. Попробовал — увы, нет :( Стек-то виден, но — бесполезный, «fall()» в нем не упоминается даже.

                Возможно ли решение в принципе?
                  +1
                  GetExceptionCode()->ExceptionRecord->ExceptionAddress будет содержать адрес инструкции, следующей за инструкцией, которая вызвала исключение. Отмапить его в имя функции (и даже номер строчки кода) можно с помощью Debug Help Library. Естественно, программа должна быть собрана с отладочной инфой.
                  +2
                  В принципе странно делать обработку подобных сигналов. А если в обработчике произойдет SIGSEGV? Вместо распечатки stack-trace проще посмотреть на корку и попытаться все понять.
                    0
                    Не понял, так что в Unix не получится словить SIGSEV и сказать «ничего не было» и продолжить работать.
                    Ситуация печальная ведь от ошибок никто не застрахован, а некоторые не критичные куски очень хочется обернуть в try/catch.

                      +3
                      Теоретически можно, но практически этого делать НЕ СЛЕДУЕТ.
                        –1
                        Как? Как только вы в обработчике назначите signal(SIGSEGV, SIG_DFL) или signal(SIGSEGV, SIG_IGN), программа завершится. Если вы этого делать не будете, обработчик будет вызываться бесконечно.
                          0
                          ну, Вы можете перезапустить main() _прямо из обработчика_

                          Но так делать НЕ СЛЕДУЕТ, как очень точно заметил Gorthauer87

                          Не следует потому, что «память-то уже порченая», и куда вы денете те 100мБ оперативы, которые занял покореженный упавший процесс, на руинах которого Вы запускаете новый — неясно.
                            0
                            ну, можно из обработчика запустить другой процесс, который вначале прибьёт покорёженный процесс, затем запустит его снова и после этого завершится. Не сказал бы, что так СЛЕДУЕТ делать, но уже вполне жизнеспособная подпорка падающему время от времени процессу.
                              0
                              Да.

                              Собственно, bash-скрипты, которые делают рестарт сервисам-падунам — это совершенно нормально и естественно :)
                            0
                            Слышал про костыль с longjmp
                              0
                              Использовать sigaction() вместо signal(). Тогда в обработчик сигнала будет передан указатель на контекст, содержащий помимо прочего значения регистров в момент исключения. Изменив в контексте значение регистра PC, по возвращении из обработчика можно попасть в любую точку программы.
                              +1
                              Можно не только теоретически, но и на практике это широко применяется в системном программировании. Например, в HotSpot JVM обработка SIGSEGV используется для ликвидации проверок на null, для ускоренной проверки на StackOverflow, для Safepoint-поллинга, для эмуляции memory barrier и для разных спекулятивных ловушек, связанных с обработкой особых ситуаций виртуальной машины без генерации дополнительного кода для обычных быстрых путей. Думаю, напишу об этих приемах отдельную статью.
                            +2
                            Какой смысл вообще обрабатывать сегфолт? Мы же все равно не сможем понять, где он произошел. Разве что включать обработку SIGSEGV в критических областях (чтобы знать, что сегфолт был там).

                              0
                              Почему не сможем? Разве backtrace в обработчике нам это не показывает?
                                +2
                                Не показывает, если воспользоваться примером, приведенным автором, получим для файла с обработчиком:

                                #include
                                #include
                                #include
                                int memento(){
                                int a=0;
                                printf(«Ooops\n»);
                                return 0;
                                }
                                void fall(){
                                int* p = 0;
                                p[5] = 13;
                                printf(«p=%d\n», p[5]);
                                }
                                void posix_death_signal(int signum){
                                memento();
                                signal(signum, SIG_DFL);
                                }
                                int main(int argc, char *argv[]){
                                signal(SIGSEGV, posix_death_signal);
                                fall();
                                }

                                вот такой вывод:
                                gcc 1.c && ./a.out
                                Ooops
                                Ошибка сегментирования

                                А для файла без обработчика:

                                #include
                                void fall(){
                                int* p = 0;
                                p[5] = 13;
                                printf(«p=%d\n», p[5]);
                                }
                                int main(int argc, char *argv[]){
                                fall();
                                }

                                вот такой вывод:
                                gcc 2.c && ./a.out
                                Ошибка сегментирования
                                  +4
                                  На хабре есть замечательный тег <source>, который позволяет вашим читателям не ломать глаза при чтении кода:

                                  #include "тут был инклюд"
                                  #include "тут тоже был инклюд"
                                  #include "тут снова был инклюд"
                                  
                                  int memento(){
                                      int a=0;
                                      printf("Ooops\n");
                                      return 0;
                                  }
                                  void fall(){
                                      int* p = 0; 
                                      p[5] = 13;
                                      printf("p=%d\n", p[5]);
                                  }
                                  void posix_death_signal(int signum){
                                      memento();
                                      signal(signum, SIG_DFL);
                                  }
                                  int main(int argc, char *argv[]){
                                      signal(SIGSEGV, posix_death_signal);
                                      fall();
                                  }
                                    +1
                                    Тут скорее вопрос в том, почему всеми этими замечательными тегами не могут пользоваться (<s>неприкасемые</s>) отхабренные. Кому от этого лучше?
                                      0
                                      Да, прошу прощения, не заглянул в профиль.
                                  • UFO just landed and posted this here
                                  • UFO just landed and posted this here
                                    +1
                                    Сможем с помощью sigaction(). Тогда в обработчик будет передана подробная информация о SIGSEGV, включая адрес инструкции, на которой свалилась программа, адрес памяти, по которому обратились, а также контекст со значениями регистров процессора в момент падения.
                                      +1
                                      А насколько сложно с его помощью получить человекопонятный листинг стека, файл с коркой и потом это всё вывести в красивом окошечке с предложением послать разработчикам?
                                        0
                                        На самом деле, составить полный и понятный отчет об ошибке довольно трудоемко. Взять, к примеру, уже упомянутые исходники HotSpot JVM — там целый модуль посвящен генерации hs_err.log в случае падения приложения. И есть свои тонкости. Например, надо учитывать, что код обработчика надо исполнять на отдельном стеке, т.к. ошибка могла быть вызвана переполнением стека; а еще, что SEGV могут возникать и внутри самого обработчка и т.д.
                                          0
                                          А не проще тогда отбросить корку и пустить новый процесс чтобы ее распарсить?
                                            0
                                            Когда дампы >10GB? Вряд ли.
                                              0
                                              В моем случае все не так печально) Но для серверного ПО это да, проблема)
                                    0
                                    вы можете уронить программу в любом потоке, обработчик запустится в любом случае

                                    Примерчик бы с несколькими потоками… Т.к. у меня там получается не все так гладко. В частности первый вопрос, в каком потоке будет запущен обработчик?
                                      0
                                      Создайте несколько функций-обработчиков, выводящих разный текст, назначайте каждому потоку свою функцию.
                                      Точно так же можно сделать несколько обработчиков (или один, чье поведение зависит от глобальной переменной, которую вы будете модифицировать перед вызовом signal(...) ) и назначать их на нужный сигнал в начале критической секции, а в ее конце сбрасывать на обработчик по умолчанию.
                                        0
                                        Вот (на коленке):

                                        #include <stdio.h>
                                        #include <signal.h>
                                        #include <pthread.h>
                                        void memento(int a){
                                        printf(«Ooops segfault in thread %d\n», a);
                                        signal(SIGSEGV, SIG_DFL);
                                        }
                                        void _1(int a){memento(1);}
                                        void _2(int a){memento(2);}
                                        void _3(int a){memento(3);}
                                        void _4(int a){memento(4);}
                                        void *fall(void *arg){
                                        signal(SIGSEGV, arg);
                                        int* p = 0;
                                        p[5] = 13;
                                        printf(«p=%d\n», p[5]);
                                        signal(SIGSEGV, SIG_DFL);
                                        }
                                        void *not_fall(void *arg){
                                        signal(SIGSEGV, arg);
                                        int n = 0;
                                        printf(«n=%d\n», n);
                                        signal(SIGSEGV, SIG_DFL);
                                        }
                                        int main(int argc, char *argv[]){
                                        pthread_t threads[4]; int i;
                                        pthread_create(&threads[1], NULL, fall, _1);
                                        pthread_create(&threads[2], NULL, not_fall, _2);
                                        pthread_create(&threads[3], NULL, not_fall, _3);
                                        pthread_create(&threads[4], NULL, fall, _4);
                                        for(i=0; i<4; i++) pthread_join(threads[i], NULL);
                                        }

                                        Выхлоп:

                                        gcc -lpthread 1.c && ./a.out
                                        n=0
                                        Ooops segfault in thread 1
                                        n=0
                                        Ooops segfault in thread 4
                                        Ошибка сегментирования
                                          0
                                          а вот мой выхлоп на этом коде:
                                          $ ./a.out 
                                          n=0
                                          Ooops segfault in thread 3
                                          n=0
                                          Ooops segfault in thread 4
                                          Ошибка сегментирования
                                          $ ./a.out 
                                          n=0
                                          Ooops segfault in thread 1
                                          n=0
                                          Ошибка сегментирования
                                          $ ./a.out 
                                          Ooops segfault in thread 1
                                          n=0
                                          n=0
                                          Ошибка сегментирования
                                          $ ./a.out 
                                          Ooops segfault in thread 1
                                          n=0
                                          n=0
                                          Ошибка сегментирования

                                          А под Win подобный код вообще ничего не ловит…
                                            0
                                            Да, виноват. Действительно ведь: обработчик сигналов получается глобальным.

                                            Так что, не годится этот способ.
                                        0
                                        Нуууу… примерчик — не примерчик… смотрите.

                                        Я пишу на QT и изначально у меня было:

                                        int fall()
                                        {
                                            QMainWindow w;
                                            w.show();
                                        }
                                        

                                        в том QMainWindow была кнопка, на обработчике которой и вызывался segfault. Обработчик кнопки работает в другом потоке, чем main()

                                        А в обработчике у меня стояло создание QDialog-а, его exec() и — только потом — падение.
                                          0
                                          Сразу скажу, меня интересует именно переносимость, что бы на разных платформах это работало одинаково, или по крайней мере, что бы не сложно было заставить работать одинаково.
                                          Так вот, мои тесты пока не позволяют этого добиться, по тому я попросил код, м.б. мой кривой :)
                                          Отсюда соответственно вопрос, на чем это воспроизводилось? В Linux в принципе да, получается отловить, и если обработчик был зарегистрирован только в одном потоке, то поведение достаточно предсказуемое.
                                            0
                                            Но вот под Win SIGSEGV пока ловиться только в случае, если он появляется в главном потоке, и обработчик зарегистрирован в нем же. Отловить же его в других потоках пока вообще никак получается, где бы я их не регистрировал…
                                        0
                                        __finally это блок, который выполнится обязательно с исключительными ситуациями или без них.

                                        Это не catch(...).

                                        Блок предназначен для освобождения ресурсов, которые мы захватили, но из-за сбоя не сможем использовать (залочили файл, выделили оперативку, выставили семафор/мьютекс и т. д.)
                                          0
                                          Кстати, вот наколенный этюдик, набросанный для объяснения другому человеку обработку исключений на плоском цэ (может кому нть поможет)

                                          FPE_INTDIV0 and similar constants are not defined for Borland Turbo C 1.0.

                                          in Turbo C 1.0 we must check floating points status with int _status87() and clear it with void _fpreset() or int _clear87().

                                          probably _status87() returns constants like EM_ZERODIVIDE or SW_OVERFLOW defined in float.h of this c-version. did not checked though. hope you got the principle.
                                          finally, you can ignore arguments in the definition of exception handler and it will do for any c and c++ version. but you can loose event details.

                                          having Win32 specific code removed, this program works fine under DOS/TurboC1 (and still Win32/BorlandC++5).

                                          #include "stdio.h"
                                          #include "conio.h"
                                          #include "signal.h"
                                          #include "setjmp.h"

                                          void CatchSIGFPE(void);

                                          jmp_buf jmp_bufWatchoutIntDiv0; /*storage of the program state, including execution point.*/
                                          /*you can dump it to external file for later analysis.*/

                                          int main(int argc, char* argv[])
                                          {
                                          int nA, nB, nC;
                                          float sA, sB, sC;

                                          /*installing handlers first time.*/
                                          signal(SIGFPE, CatchSIGFPE);

                                          nA = 1; nB = 1; nC = 0;
                                          sA = 1.0; sB = 1.0; sC = 0.0;

                                          /*we don't raise the exception ourselves, but the division actually does it.*/
                                          if ( !setjmp(jmp_bufWatchoutIntDiv0) )
                                          nA = nB / nC; /*raises exception SIGFPE.*/
                                          else
                                          nA = 0, printf("\nNah, checked that, but it scared me! I set result of division to 0.");

                                          /*checking wether exception catcher is still in our hands.*/
                                          if ( !setjmp(jmp_bufWatchoutIntDiv0) )
                                          sA = sB / sC; /*raises exception SIGFPE.*/
                                          else
                                          sA = 0, printf("\nAgain, I was close to crash! I set result to zero one more time.");

                                          printf("\nWOW!.. That was fun! :-)");
                                          printf("\nPress any key...");
                                          getch();

                                          return 0;
                                          };

                                          void CatchSIGFPE(void)
                                          {
                                          signal(SIGFPE, CatchSIGFPE);
                                          fprintf(stderr, "\n>SIGFPE caught!");
                                          longjmp(jmp_bufWatchoutIntDiv0, 1);
                                          };
                                          0
                                            0
                                            Мне всегда было интересно, что вы будете делать после этого? Умирать более красиво?
                                              +1
                                              Хотя бы писать логи и отправлять куда следует (с)
                                                0
                                                Например освободить ресурсы или просто потянуть время, что бы другие потоки это сделали (при sigsegv в одном потоке, другие потоки продолжают работать до тех пор, пока не завершит свою обработчик этого сигнала). Предположим используются именованные семафоры, один поток периодически захватывает и отпускает их (хотя это не очень типичное применение семафоров, тут скорее больше бы мютексы подошли, но в posix например нет именованных мютексов). Тут вдруг другой поток падает, и уносит за собой поток, который захватил семафор, но не отпустил, хотя в принцип тот бы мог бы без проблем дойти до точки, в которой возможно корректное завершение.
                                                0
                                                А вы не думали, что в случае сегфолта программа категорически ничего не должна сохранять?
                                                Ведь данные уже находятся в неизвестном состоянии.
                                                  0
                                                  Скажу за unix: sigsegv и прочие «плохие» сигналы нужно обрабатывать выходом из программы, можно перед этим в лог что-нить написать.
                                                  можно:
                                                  1) ловить sigsegv через gdb
                                                  2) позволить создавать коре-файлы

                                                  в тестовом варианте я иногда вставляю raise(SIGSTOP);, чтобы процесс остановился и была возможность к нему приаттачиться.
                                                    0
                                                    Общий синтаксис обработки исключений выглядит следующим образом:

                                                    Нет, это не так. Каждому блоку __try{...} должен соответствовать либо один блок __except(...){...}, либо один блок __finally{...}. При этом допускаются вложенные блоки. Но синтаксиса в стиле «несколько catch паровозиком» как в чистых плюсах нет.
                                                    См., например, Рихтер «Windows via C/C++», 769с:
                                                    Обратите внимание на ключевое слово __except. За блоком try всегда должен следовать либо блок finally, либо блок except. Для данного блока try нельзя указать одновременно и блок finally, и блок except; к тому же за try не может следовать несколько блоков finally или except. Однако try-finally можно вложить в try-except, и наоборот.

                                                    Only users with full accounts can post comments. Log in, please.