Привет, уважаемый читатель!
Все разработчики программ рано или поздно сталкиваются с проблемой падения программы у пользователя. Но далеко не все при этом могут получить доступ к конкретному компу, на котором что-то идёт не так, запустить там 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(i = 0; i < frames; ++i)
{
fprintf(f, "%sn", 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, "%stt%sn", 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, "%sn", "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, "%sn", "That's a function!");
doSomethingPrivate();
}
private:
void doSomethingPrivate()
{
// crash? oh, no...
fprintf(stderr, "%s myPrivateInteger == %dn", "That's a private function!", myPrivateInteger);
fprintf(stderr, "myPrivateDoubles[1] == %fn", myPrivateDoubles[1]);
fprintf(stderr, "myPrivateString == %pn", 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
'ом, указывающим на нашу строку, и наши переменные класса будут указывать на части этой строки и содержать, соответственно, любой мусор, входящий в неё. А в первом случае указатель, скорее всего, просто будет ссылаться на недоступную для программы область памяти, поэтому закрашится уже первый вызов приватной функции.
К чему в данной статье описание столь элементарных вещей? Ну как же, надо ведь показать, как программы крашить! И объяснить, почему вызов методов классов по невалидным указателям не всегда приводит к крашу. Если интересен полный код, прошу, как всегда, на гитхаб.
В общем, удачной отладки! И поменьше краш-репортов ;)
Автор: silvansky