Как стать автором
Обновить

Комментарии 59

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

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

Расскажите — вкратце?
В 2х словах не описать, но на статью не тянет, да и вроде есть несколько статей уже по этой тематике.
Насколько я знаю, в 64х битном коде можно ловить SEH исключения, сгенерированные в 32х битном, но нельзя их пробрасывать выше по стеку, если там 32х битный код. Это проявляется, например, в том, что в 32х битных оконных приложениях исключения, выброшенные в обработчиках оконных сообщений, т.е. из стека DispatchMessage, просто проглатываются операционной системой. Т.е. если в приложении, запущенном под Windows XP, при клике по кнопке вываливается сообщение об ошибке, то в Windows 7 x64 не произойдет ничего. И потенциально приложение может перейти в несогласованное состояние, если код был написан в расчете на перехват исключений. Ситуация редкая, но неприятная.
В случае gcc под x86 имеются замечательные встроенные функции backtrace и backtrace_symbols, которые позволяют получить читабельный бектрейс в конкретной точке кода, в том числе и в обработчике SIGSEGV.
Жаль, что в gcc под ARM такого нет и приходится извращаться с ассемблером…
Да что вы говорите.
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
хм, лишних переносов строк в предпросмотре не было
читаем ман: 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.
Вы меня неправильно поняли. Я отвечал на утверждение что в gcc для ARM backtrace() не работает.

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

А символов нет потому что я backtrace_symbols() не вызываю :) Если вызвать — появятся. Конечно, придется добавить что-то вроде -rdynamic, чтобы нужные символы оказались в динамической таблице.
Если уж речь зашла даже о реализации __try{}__except блоков, то почему не упомянули стандартный интерфейс SetUnhandledExceptionFilter?
Он как раз ближе к обработке в стиле posix, которую Вы хвалите и реализуется системой, а не компилятором.
Прикольно!

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

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

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

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

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

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

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

#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
Ошибка сегментирования
На хабре есть замечательный тег <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();
}
Тут скорее вопрос в том, почему всеми этими замечательными тегами не могут пользоваться (<s>неприкасемые</s>) отхабренные. Кому от этого лучше?
Да, прошу прощения, не заглянул в профиль.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Сможем с помощью sigaction(). Тогда в обработчик будет передана подробная информация о SIGSEGV, включая адрес инструкции, на которой свалилась программа, адрес памяти, по которому обратились, а также контекст со значениями регистров процессора в момент падения.
А насколько сложно с его помощью получить человекопонятный листинг стека, файл с коркой и потом это всё вывести в красивом окошечке с предложением послать разработчикам?
На самом деле, составить полный и понятный отчет об ошибке довольно трудоемко. Взять, к примеру, уже упомянутые исходники HotSpot JVM — там целый модуль посвящен генерации hs_err.log в случае падения приложения. И есть свои тонкости. Например, надо учитывать, что код обработчика надо исполнять на отдельном стеке, т.к. ошибка могла быть вызвана переполнением стека; а еще, что SEGV могут возникать и внутри самого обработчка и т.д.
А не проще тогда отбросить корку и пустить новый процесс чтобы ее распарсить?
Когда дампы >10GB? Вряд ли.
В моем случае все не так печально) Но для серверного ПО это да, проблема)
вы можете уронить программу в любом потоке, обработчик запустится в любом случае

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

#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
Ошибка сегментирования
а вот мой выхлоп на этом коде:
$ ./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 подобный код вообще ничего не ловит…
Да, виноват. Действительно ведь: обработчик сигналов получается глобальным.

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

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

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

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

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

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

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

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);
};
Мне всегда было интересно, что вы будете делать после этого? Умирать более красиво?
Хотя бы писать логи и отправлять куда следует (с)
Например освободить ресурсы или просто потянуть время, что бы другие потоки это сделали (при sigsegv в одном потоке, другие потоки продолжают работать до тех пор, пока не завершит свою обработчик этого сигнала). Предположим используются именованные семафоры, один поток периодически захватывает и отпускает их (хотя это не очень типичное применение семафоров, тут скорее больше бы мютексы подошли, но в posix например нет именованных мютексов). Тут вдруг другой поток падает, и уносит за собой поток, который захватил семафор, но не отпустил, хотя в принцип тот бы мог бы без проблем дойти до точки, в которой возможно корректное завершение.
А вы не думали, что в случае сегфолта программа категорически ничего не должна сохранять?
Ведь данные уже находятся в неизвестном состоянии.
Скажу за unix: sigsegv и прочие «плохие» сигналы нужно обрабатывать выходом из программы, можно перед этим в лог что-нить написать.
можно:
1) ловить sigsegv через gdb
2) позволить создавать коре-файлы

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

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

Публикации

Истории