Привет, уважаемый хабраюзер!
Все разработчики программ рано или поздно сталкиваются с проблемой падения программы у пользователя. Но далеко не все при этом могут получить доступ к конкретному компу, на котором что-то идёт не так, запустить там gdb и повторить падение. И даже получить информацию от пользователя бывает крайне сложно: в багтрекер (или техподдержку) приходит сообщение а-ля «программа падает, что делать?», а вот технической информации, так важной для разработчика, пользователь не прилагает к своему сообщению. Да ещё и не каждый напишет об этом! Просто перестанет пользоваться программой — и всё.
Некоторые ОС предлагают отправить краш-репорт разработчикам. Но! Разработчикам ОС, а не Вам, то есть совсем не тем людям, которым это действительно нужно! И тут на помощь приходят собственные краш-репорты, которая Ваша программа должна бы отправить на Ваш сервер. Но как их сделать? Как правильно обработать SEGFAULT и при этом отправить вразумительную информацию разработчику?
На Хабре уже была интересная статья от Arenim, посвящённая обработке крашей. Вкратце повторю суть: мы ловим POSIX-сигнал SIGSEGV, а после его обработки выходим из программы.
Теперь дело за малым: локализовать проблему! И хотя указанный выше способ работает и в Windows, нормальный backtrace мы можем получить только в *nix (на самом деле, можно его получить и в винде, но для этого придётся распространять дебажную сборку, что не очень хорошо). Итак, курим мануалы и делаем вот что:
И всё, репорт ушёл на сервер! Если хочется, можно перед отправкой спросить пользователя — а не отправить ли нам репортик? Конечно, в GUI-программе это немного опасно — ведь после SEGFAULT'а адекватность внутреннего состояния графического фреймворка (ну или голых иксов) не гарантируется, так что тут лучше пользователя предупредить заранее (в лицензионном соглашении, к примеру) и поставить в настройки галочку «отправлять анонимные репорты». Главное — не вписывать в репорт личной информации пользователя и прочих данных, это не только аморально, но и может преследоваться по закону (если, конечно, в конце лицензионного соглашения мелкими буквами не прописано согласие пользователя на это).
Испытаем теперь изложенный метод на практике. Создадим простенькую программу с простеньким классом и простенькими дополнительными функциями. И попробуем этот код уронить. Самое простое — вызвать метод у нулевого указателя на класс, но это слишком примитивно, пусть лучше указатель указывает «в небо», так интереснее. Как этого добиться? Ну конечно же применить всеми нами так горячо любимый
Что ж, похоже, что мы кастанём к нашему классу
Заметим, что в функции
Можно теперь собрать и запустить нашу программу. И что же мы увидим? Программа отработала успешно, но
А видим мы строчку
Программа ведь крашится (скорее всего) на вызове
Давайте для начала поэкспериментируем, не будем при вызове
Видно, что крашится на втором вызове
Итак, почему же даже при вызове метода у нулевого указателя сегфолт возникает только на второй функции? Чем они отличаются? Опытные плюсоводы уже давно догадалисьи не читают эту статью, а для остальных поясню. Они отличаются использованием переменных класса! Если переменные не используются, то абсолютно не важно, у какого указателя вызывать функцию, ведь скрытый параметр
К чему в данной статье описание столь элементарных вещей? Ну как же, надо ведь показать, как программы крашить! И объяснить, почему вызов методов классов по невалидным указателям не всегда приводит к крашу. Если интересен полный код, прошу, как всегда, на гитхаб.
В общем, удачной отладки! И поменьше краш-репортов ;)
Все разработчики программ рано или поздно сталкиваются с проблемой падения программы у пользователя. Но далеко не все при этом могут получить доступ к конкретному компу, на котором что-то идёт не так, запустить там gdb и повторить падение. И даже получить информацию от пользователя бывает крайне сложно: в багтрекер (или техподдержку) приходит сообщение а-ля «программа падает, что делать?», а вот технической информации, так важной для разработчика, пользователь не прилагает к своему сообщению. Да ещё и не каждый напишет об этом! Просто перестанет пользоваться программой — и всё.
Некоторые ОС предлагают отправить краш-репорт разработчикам. Но! Разработчикам ОС, а не Вам, то есть совсем не тем людям, которым это действительно нужно! И тут на помощь приходят собственные краш-репорты, которая Ваша программа должна бы отправить на Ваш сервер. Но как их сделать? Как правильно обработать SEGFAULT и при этом отправить вразумительную информацию разработчику?
На Хабре уже была интересная статья от Arenim, посвящённая обработке крашей. Вкратце повторю суть: мы ловим POSIX-сигнал SIGSEGV, а после его обработки выходим из программы.
void catchCrash(int signum)
{
reportTrouble(); // отправляем краш-репорт
signal(signum, SIG_DFL); // перепосылаем сигнал
exit(3); //выходим из программы
}
int main()
{
signal(SIGSEGV, catchCrash);
//-- ... --//
}
Теперь дело за малым: локализовать проблему! И хотя указанный выше способ работает и в Windows, нормальный backtrace мы можем получить только в *nix (на самом деле, можно его получить и в винде, но для этого придётся распространять дебажную сборку, что не очень хорошо). Итак, курим мануалы и делаем вот что:
void reportTrouble()
{
void *callstack[128];
int frames = backtrace(callstack, 128);
char **strs=backtrace_symbols(callstack, frames);
// тут выводим бэктрейс в файлик crash_report.txt
// можно так же вывести и иную полезную инфу - версию ОС, программы, etc
FILE *f = fopen("crash_report.txt", "w");
if (f)
{
for(int i = 0; i < frames; ++i)
{
fprintf(f, "%s\n", strs[i]);
}
fclose(f);
}
free(strs);
system("curl -A \"MyAppCrashReporter\" --form report_file=@\"crash_report.txt\" http://reports.myserver.com");
}
И всё, репорт ушёл на сервер! Если хочется, можно перед отправкой спросить пользователя — а не отправить ли нам репортик? Конечно, в GUI-программе это немного опасно — ведь после SEGFAULT'а адекватность внутреннего состояния графического фреймворка (ну или голых иксов) не гарантируется, так что тут лучше пользователя предупредить заранее (в лицензионном соглашении, к примеру) и поставить в настройки галочку «отправлять анонимные репорты». Главное — не вписывать в репорт личной информации пользователя и прочих данных, это не только аморально, но и может преследоваться по закону (если, конечно, в конце лицензионного соглашения мелкими буквами не прописано согласие пользователя на это).
Испытаем теперь изложенный метод на практике. Создадим простенькую программу с простеньким классом и простенькими дополнительными функциями. И попробуем этот код уронить. Самое простое — вызвать метод у нулевого указателя на класс, но это слишком примитивно, пусть лучше указатель указывает «в небо», так интереснее. Как этого добиться? Ну конечно же применить всеми нами так горячо любимый
reinterpret_cast
! И вот, чтобы бэктрейс был интереснее, создаём функции goCrash()
и crash(void *)
.int crash(void *obj)
{
Crasher *crasher = reinterpret_cast<Crasher *>(obj);
crasher->doSomething();
return -1;
}
void goCrash()
{
const char *str = "Hello, crash!";
const char *str2 = "Hello again, crash!";
char str3[200];
sprintf(str3, "%s\t\t%s\n", str, str2);
long long add = rand() % 20000 + 1500234000l;
// fire in my leg!
crash(reinterpret_cast<void *>(str3 - add));
}
Что ж, похоже, что мы кастанём к нашему классу
Crasher
некий заранее не известный адрес. Весьма любопытно! Давайте же класс объявим:#define P_DOUBLE_COUNT 10000
class Crasher
{
public:
// c-tor
Crasher()
{
myPrivateString = new char[100];
sprintf(myPrivateString, "%s\n", "that\'s my private string!");
myPrivateInteger = 100;
for (int i = 0; i < P_DOUBLE_COUNT; ++i)
myPrivateDoubles[i] = i / 100.0;
}
// func
void doSomething()
{
// here we can (?) crash
fprintf(stderr, "%s\n", "That\'s a function!");
doSomethingPrivate();
}
private:
void doSomethingPrivate()
{
// crash? oh, no...
fprintf(stderr, "%s myPrivateInteger == %d\n", "That\'s a private function!", myPrivateInteger);
fprintf(stderr, "myPrivateDoubles[1] == %f\n", myPrivateDoubles[1]);
fprintf(stderr, "myPrivateString == %p\n", myPrivateString);
// still alive? crash! crash! crash!
((Crasher*)NULL)->doSomething();
}
private:
char *myPrivateString;
int myPrivateInteger;
double myPrivateDoubles[P_DOUBLE_COUNT];
};
Заметим, что в функции
doSomethingPrivate()
у нас всё ж вызывается функция у нулевого указателя. Так, на всякий случай. Вдруг после вызова doSomething()
для неопределённого адреса программа ещё выживет?Можно теперь собрать и запустить нашу программу. И что же мы увидим? Программа отработала успешно, но
curl
ругнулся, что сервер не найден. Ну да это ерунда, можно временно заменить его вызов на cat crash_report.txt
дабы лицезреть наш краш-репорт сразу же. Итак, что ещё мы видим?А видим мы строчку
"That's a function!"
, выведенную из метода doSomething()
! Интересно, не правда ли? Указатель указывает в небо, а методы работают? Ну, не совсем так.Программа ведь крашится (скорее всего) на вызове
doSomethingPrivate()
, и бэктрейс нам об этом красноречиво докладывает:0 segfault 0x000000010d0a98c8 _Z13reportTroublev + 40
1 segfault 0x000000010d0a99d0 _Z10catchCrashi + 16
2 libsystem_c.dylib 0x00007fff99b5dcfa _sigtramp + 26
3 ??? 0x00007fff00000000 0x0 + 140733193388032
4 segfault 0x000000010d0a9c67 _ZN7Crasher11doSomethingEv + 71
5 segfault 0x000000010d0a9880 _Z5crashPv + 32
6 segfault 0x000000010d0a9ac7 _Z7goCrashv + 199
7 segfault 0x000000010d0a9b33 main + 67
8 segfault 0x000000010d0a9854 start + 52
Давайте для начала поэкспериментируем, не будем при вызове
crash()
добавлять лишний сдвиг адреса, что выведет программа? Где крашнется? Кхм!That's a function!
That's a private function! myPrivateInteger == 1752392050
myPrivateDoubles[1] == 60993401604041306737928347282702617388988841504491171140800281285302442927306116721201046092641903128620672849302937378251940003901836219046866981678295779355600933772275817062376375849852470059862498765690530537583237171035779906888043337758015488.000000
myPrivateString == 0x63202c6f6c6c6548
That's a function!
0 segfault 0x0000000109a5e8c8 _Z13reportTroublev + 40
1 segfault 0x0000000109a5e9d0 _Z10catchCrashi + 16
2 libsystem_c.dylib 0x00007fff99b5dcfa _sigtramp + 26
3 ??? 0x0000040000000000 0x0 + 4398046511104
4 segfault 0x0000000109a5ec67 _ZN7Crasher11doSomethingEv + 71
5 segfault 0x0000000109a5ec1a _ZN7Crasher18doSomethingPrivateEv + 208
6 segfault 0x0000000109a5ec67 _ZN7Crasher11doSomethingEv + 71
7 segfault 0x0000000109a5e880 _Z5crashPv + 32
8 segfault 0x0000000109a5eac4 _Z7goCrashv + 196
9 segfault 0x0000000109a5eb33 main + 67
10 segfault 0x0000000109a5e854 start + 52
Видно, что крашится на втором вызове
doSomethingPrivate()
, а первый прошёл на ура, хотя и вывел нам не совсем то, что задумывалось.Итак, почему же даже при вызове метода у нулевого указателя сегфолт возникает только на второй функции? Чем они отличаются? Опытные плюсоводы уже давно догадались
this
не используется, а именно в нём у нас лежит мусор. Во втором примере (без сдвига) вызывается приватная функция с this
'ом, указывающим на нашу строку, и наши переменные класса будут указывать на части этой строки и содержать, соответственно, любой мусор, входящий в неё. А в первом случае указатель, скорее всего, просто будет ссылаться на недоступную для программы область памяти, поэтому закрашится уже первый вызов приватной функции.К чему в данной статье описание столь элементарных вещей? Ну как же, надо ведь показать, как программы крашить! И объяснить, почему вызов методов классов по невалидным указателям не всегда приводит к крашу. Если интересен полный код, прошу, как всегда, на гитхаб.
В общем, удачной отладки! И поменьше краш-репортов ;)