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

в 11:34, , рубрики: c++, curl, nix, Программирование, метки: , ,

Привет, уважаемый читатель!

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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js