Краш-репорты в *nix: backtrace, SEGFAULT (и reinterpret_cast)

  • Tutorial
Привет, уважаемый хабраюзер!

Все разработчики программ рано или поздно сталкиваются с проблемой падения программы у пользователя. Но далеко не все при этом могут получить доступ к конкретному компу, на котором что-то идёт не так, запустить там 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'ом, указывающим на нашу строку, и наши переменные класса будут указывать на части этой строки и содержать, соответственно, любой мусор, входящий в неё. А в первом случае указатель, скорее всего, просто будет ссылаться на недоступную для программы область памяти, поэтому закрашится уже первый вызов приватной функции.

К чему в данной статье описание столь элементарных вещей? Ну как же, надо ведь показать, как программы крашить! И объяснить, почему вызов методов классов по невалидным указателям не всегда приводит к крашу. Если интересен полный код, прошу, как всегда, на гитхаб.

В общем, удачной отладки! И поменьше краш-репортов ;)
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

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

    +3
    Спасибо — в закладки! Интересно, а в mingw есть какие-нибудь механизмы для поддержания syslog || dtrace(systemtap)?
      +1
      в reportTrouble надо i объявить.

      Большое спасибо, честно говоря. Среди себя поднимал вопрос давным давно, сравнивая отлаживаемость Java с голыми С. И к тому же backtrace приходил, но сильно далеко не убежал.

      А ваш код прямо сейчас скопипастил, спасибо :-)
        0
        Только ещё сразу лучше к имени файла репорта добавлять pid
          0
          Это уже на усмотрение разработчика. В моей программе вообще добавляется куча информации о системе (версия, битность, язык и тп), а так же уникальный GUID системы.
          0
          i объявил, спасибо! =)
          +2
          А откуда backtrace возьмет имена функций на стрипнутом экзешнеке?
            0
            Как минимум, по классам он всё показывает:

            0   segfault                            0x0000000102b1a8c8 segfault + 2248
            1   segfault                            0x0000000102b1a9d0 segfault + 2512
            2   libsystem_c.dylib                   0x00007fff99b5dcfa _sigtramp + 26
            3   ???                                 0x0000040000000000 0x0 + 4398046511104
            4   segfault                            0x0000000102b1ac67 _ZN7Crasher11doSomethingEv + 71
            5   segfault                            0x0000000102b1ac1a _ZN7Crasher18doSomethingPrivateEv + 208
            6   segfault                            0x0000000102b1ac67 _ZN7Crasher11doSomethingEv + 71
            7   segfault                            0x0000000102b1a880 segfault + 2176
            8   segfault                            0x0000000102b1aac4 segfault + 2756
            9   segfault                            0x0000000102b1ab33 segfault + 2867
            10  segfault                            0x0000000102b1a854 segfault + 2132
            

            Чаще всего в плюсовой программе именно это и требуется.
              0
              А после того, как над экзешником поработал strip?
                0
                Это и есть после стрипа:

                $ strip segfault 
                $ ./segfault 
                That's a function!
                That's a private function! myPrivateInteger == 1752392050
                myPrivateDoubles[1] == 60993401604041306737928347282702617388988841504491171140800281285302442927306116721201046092641903128620672849302937378251940003901836219046866981678295779355600933772275817062376375849852470059862498765690530537583237171035779906888043337758015488.000000
                myPrivateString == 0x63202c6f6c6c6548
                That's a function!
                0   segfault                            0x0000000102b1a8c8 segfault + 2248
                1   segfault                            0x0000000102b1a9d0 segfault + 2512
                2   libsystem_c.dylib                   0x00007fff99b5dcfa _sigtramp + 26
                3   ???                                 0x0000040000000000 0x0 + 4398046511104
                4   segfault                            0x0000000102b1ac67 _ZN7Crasher11doSomethingEv + 71
                5   segfault                            0x0000000102b1ac1a _ZN7Crasher18doSomethingPrivateEv + 208
                6   segfault                            0x0000000102b1ac67 _ZN7Crasher11doSomethingEv + 71
                7   segfault                            0x0000000102b1a880 segfault + 2176
                8   segfault                            0x0000000102b1aac4 segfault + 2756
                9   segfault                            0x0000000102b1ab33 segfault + 2867
                10  segfault                            0x0000000102b1a854 segfault + 2132
                
                  +1
                  Впишите в начало main.cpp
                  pragma GCC visibility push(hidden)
                  и повторите эксперимент.
                  У вас по-умолчанию все символы доступны для динамической загрузки с помощью dladdr.
                    0
                    Результат аналогичный указанному в комменте выше: все символы скрыты, кроме методов класса.
                      +1
                      diff --git a/main.cpp b/main.cpp
                      index 9f89031..1e8ba47 100644
                      --- a/main.cpp
                      +++ b/main.cpp
                      @@ -3,6 +3,8 @@
                       #include <signal.h>
                       #include <execinfo.h>
                       
                      +#pragma GCC visibility push(hidden)
                      +
                       #define P_DOUBLE_COUNT   10000
                       
                       // if set to 1, cat is used instead of curl'ing file to server
                      -- 
                      


                      Результат (после стрипа):
                      0   segfault                            0x0000000100000a7c 0x0 + 4294969980
                      1   segfault                            0x0000000100000b19 0x0 + 4294970137
                      2   libSystem.B.dylib                   0x00007fff831fd1ba _sigtramp + 26
                      3   ???                                 0x3838383630393937 0x0 + 4051049670208207159
                      4   segfault                            0x0000000100000c9b 0x0 + 4294970523
                      5   segfault                            0x0000000100000c61 0x0 + 4294970465
                      6   segfault                            0x0000000100000c9b 0x0 + 4294970523
                      7   segfault                            0x0000000100000a59 0x0 + 4294969945
                      8   segfault                            0x0000000100000b99 0x0 + 4294970265
                      9   segfault                            0x0000000100000bd5 0x0 + 4294970325
                      10  segfault                            0x0000000100000a34 0x0 + 4294969908
                      11  ???                                 0x0000000000000001 0x0 + 1
                      
                        0
                        Да, действительно, прошу прощения за допущенную ошибку — забыл собрать проект после вставки прагмы.

                        Кстати, не знал о такой. Часто ли используется в реальных проектах?
                          0
                          Обычно используется linker script чтобы что попало не экспортировалось.
                            0
                            Что становится нетривиальной задачей в случае C++.
                              0
                              Есть проблемы с explicitly instantiated templates, но такие вещи почти никогда не экспортируются. Всё остальное нормально работает.
                                0
                                Ну вот хотим мы класс экспортировать, не дай бог в неймспейсе, с кучей операторов и перегруженных функций. А еще хорошо бы экспортировать vtable и typeinfo от этого класса. Руками я это прописывать в линкер-скрипте не решусь.
                                  0
                                  Может мы о разных вещах говорим. Я имею ввиду ld --version-script=version-scriptfile который вполне успешно понимает C++ неймспейсы и классы.

                                  Например экспорт всего класса Bar из namespace Foo, кроме символов начинающихся с private_stuff.
                                  LIBNAME_123 {
                                    global:
                                      # C++ functions
                                      extern "C++" {
                                          Foo::Bar::*;
                                      };
                                      # C functions
                                      cfoo; cbar;
                                    local: *;
                                      extern "C++" {
                                          Foo::Bar::private_stuff*;
                                      };
                                  };
                                  
                                    0
                                    Посыпаю голову пеплом; о такой возможности я не знал.
                            0
                            Не далее чем на прошедшей неделе обнаружил в составе Mac OS X 10.7.4 модуль на C++ с той же самой проблемой — экспортируются абсолютно все C++ символы. При чем речь идет о модуле, исходники которого не доступны.

                            С символами код элементарно реверсится по дизасму (например всякие STL-ные вещи сразу же отбрасываются и время на анализ их кода не тратится).

                            Если вы комерческий вендор, и ваши исходные тексты закрыты, врядли вы захотите настолько облегчить реверсерам жизнь.
              +1
              fopen, насколько я помню, работает с динамической памятью. Я бы не стал такое использовать в обработчике SIGSEGV — ведь сам краш мог произойти из-за расстрела или нехватки памяти.
                0
                А что тогда использовать?
                Интересуюсь с целью улучшить свою реализацию.
                  +1
                  в своем коде — переменные на стеке и статически аллоцированные. для общения с системой — open()/read()/write() и другие API которые заведомо не используют динамическую память
                    0
                    Спасибо, поэкспериментирую!
                      0
                      Поглядите в статью Практика работы с сигналами, в особенности в комментарии. На обработчики сигнала наложены жесточайшие ограничения :)

                      НО! В случае полного краха приложения, уж лучше что то (шанс что весь ваш обработчик отработаете и вы получите репорт) чем ничего (вы даже не пытаетесь его отправит, потому что следуете всем правилам написания обработчика сигналов)
                        0
                        Я читал, да, спасибо.
                        Перечитаю ещё разок — уж очень интересная статейка.

                        И с Вашим «НО!» я полностью согласен, именно из этих соображений я и исходил при написании сего поста.
                    +1
                    А еще backtrace_symbols_fd, чтобы писать сразу в файл.
                      0
                      Ну, сразу в файл хорошо писать если ничего кроме bt там не будет. К примеру, я bt упаковываю в xml вместе с последними логами и инфо о системе.
                    0
                    ЕМНИП, stdio работает поверх выделенных заранее буферов. Но вопрос юзания сложных функций из обработчиков сигнала — конечно, тоже занятен :-)
                    +7
                    Для создания краш-репортов можно использовать google-breakpad, разработанный специально для этих целей. К тому же он кросс-платформенный.
                      0
                      Спасибо, обязательно посмотрю в его сторону.
                      +2
                      Еще бы деманглинг добавить.
                        +1
                        Я иногда делаю так

                        static int inSigSegvHandler = 0;
                        static void sigsegvHandler(int sig) {
                            ++inSigSegvHandler;
                            if(inSigSegvHandler > 1) { // защита от двойного попадания, сигнал ставлю sigaction + SA_RESTART
                                fprintf(stderr, "*** SIG %d TWICE ***\n", sig);
                                _exit(2);
                            }
                        
                            invokeDebuggerToSelf();
                            _exit(1);
                        }
                        


                        Где функция

                        void invokeDebuggerToSelf() {
                            int pid = getpid();
                            char link[MAX_PATH];
                            char buf[MAX_PATH]; 
                            char bufexe[MAX_PATH]; 
                            char pidbuf[16];
                            char cmdline[MAX_PATH]; 
                        
                            memset(bufexe, 0, sizeof(bufexe));
                            memset(link, 0, sizeof(link));
                            sprintf(link, "/proc/%d/exe", pid);
                            if(readlink(link, bufexe, MAX_PATH) <=0)
                                strcpy(bufexe, "где-там-оно-лежит"); // вдруг звезды сойдутся неудачно
                        
                            sprintf(pidbuf, "%d", pid);
                        
                            int wpid;
                            if((wpid=fork()) == 0) {
                                execlp("gdb", "gdb", "-batch", "-q", "-x", gdbFile, bufexe, pidbuf, NULL);
                            }
                            else if(wpid > 0) {
                                int status;
                        	waitpid(wpid, &status, WUNTRACED);
                        	_exit(0);
                            }
                        }
                        


                        Этот код я честно слямзил не помню где много лет назад, ну и обрезал по максимуму под свои нужды.

                        В демоне — очень даже помогает, тут и деманглинг и прочее, и 3rd party код показывает с именами и номерами строк. cmdFile содержит список нужных мне команд, в общем случае там bt + quit.

                        Главное — strip не увлекаться ;-)
                          0
                          Да, интересный подход, я думал о вызове gdb. Проблема в том, что не у всех конечных пользователей он есть. Хотя, для демона вызов gdb + отправка репорта + перезапуск — это очень даже хорошо.
                          +5
                          > нормальный backtrace мы можем получить только в *nix (на самом деле, можно его получить и в винде, но для этого придётся распространять дебажную сборку, что не очень хорошо)

                          И в виндовс можно подойти к этой проблеме. При компиляции release в visual studio создавать linker map file. А уже на стороне сервера по соответствующему map file переводить адреса в читаемый разработчиками backtrace. И раздаем клиентам release только и разработчики довольны.
                            0
                            О как, спасибо, буду знать! И попробую в проекте заимплементить.
                              +2
                              В винде можно не просто подойти к этой проблеме, а заюзать DbgHelp API. Если собирать проект студией, то она создает приличный по объему файл с символами *.pdb, который содержит символы и их привязки к релизному коду. Сам релизный код символов не содержит (кроме экспортных). При краше мы используем функцию MiniDumpWriteDump. Имея *.pdb файл символов, исходники и релизный образ у себя на машине открываем в студии дамп и получаем интерфейс отладчика, с состоянием, как будто бы крэш случился только что на нашей машине под отладчиком. Помимо стек-трейса, можно просмотреть значения локальных переменных, потоки и пр. инфу. Если использовать флаг MiniDumpWithFullMemory, то (судя по справке) вообще всю user-mode память процесса можно получить.
                                0
                                Спасибо, так тоже попробуем =)
                              +2
                              Все уже придумано:
                              code.google.com/p/google-breakpad/

                              Причем если все аккуратно настроить — буде отлично работать на релизной версии без символов
                                0
                                Спасибо, буду смотреть.
                                0
                                Крэша в случае вызова метода через nullptr не будет только если метод невиртуальный, если виртуальный — тоже будет крэш. Ну это так, дополнение.
                                  0
                                  Спасибо за дополнение!

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое