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



    
rambler's top100 Rambler's Top100