(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

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

Источник: habrahabr
silvansky



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

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

В общем, удачной отладки! И поменьше краш-репортов ;)

Ссылки по теме


 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 24.07.2012 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Microsoft 365 Business Standard (corporate)
Microsoft Windows Professional 10, Электронный ключ
Microsoft 365 Apps for business (corporate)
Microsoft 365 Business Basic (corporate)
Microsoft Office 365 для Дома 32-bit/x64. 5 ПК/Mac + 5 Планшетов + 5 Телефонов. Подписка на 1 год.
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Безопасность компьютерных сетей и защита информации
Новости ITShop.ru - ПО, книги, документация, курсы обучения
Программирование на Microsoft Access
CASE-технологии
Утиль - лучший бесплатный софт для Windows
Delphi - проблемы и решения
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100