- PVSM.RU - https://www.pvsm.ru -
Не так давно мы в компании задумали дать возможность пользователям посылать нам уведомления о произошедших ошибках в нашем ПО. Сказано — сделано. Но тут возникла задача получения backtrace-а текущего стека вызовов программы прямо в рантайме. Оказалось, что есть несколько способов решения этой задачи. Данная статья — результат моих исследований вопроса получения бэктрейса для программ написанных на С/C++ и работающих на Linux и FreeBSD.
В принципе, получить цепочку вызовов довольно просто. Вся необходимая информация хранится в стеке программы. Современные компиляторы для вызова функции формируют так называемые фреймы стека (stack frame). В начале каждого фрейма находится адрес предыдущего. А непосредственно перед фреймом сохранен адрес возврата, т.е. адрес инструкции, которая будет выполнена следующей, после завершения функции. Таким образом, все, что необходимо сделать — это пройти по списку фреймов и распечатать адреса возврата.
Например, это можно сделать так (пример для amd64):
Функцию можно было написать и короче, т.к. возвращаемое значение кладется в регистр rax, можно было обойтись и без переменной res.
Но, лично для меня, наличие ассемблерных вставок не есть истинный путь джедая. Поэтому я пошел искать другое решение.
Первое, на что я набрел, это функция __builtin_return_address добродушно предоставленная нам создателями gcc. Вот выдержка из ее полного описания:
void * __builtin_return_address (unsigned int level) — возвращает адрес возврата функции. Для level=0 функция вернет адрес возврата текущей функции, для level=1 адрес возврата функции вызвавшей текущую функцию и т.д.
При ее использовании есть только одно но: функция компилируясь разворачиваются в кучу строк ассемблерного кода (чем дальше по стеку идем, тем больше строк) и в связи с этим, она не умеют принимать переменную в качестве параметра. Поэтому вместо красивой записи вида:
return __builtin_return_address(i);
приходится писать некрасивые:
Уже лучше. Идем дальше.
В Linux стандартная библиотека предоставляет программисту целый набор функций, позволяющих получать нужную нам информацию. В FreeBSD для этих целей необходимо установить библиотеку libexecinfo. Вот они:
int backtrace(void **buffer, int size) — функция, заполняющая buffer backtrace-ом вызывающей программы.
char **backtrace_symbols(void *const *buffer, int size) — функция, принимающая результат первой функции и транслирующая адреса функций в текстовое представление.
void backtrace_symbols_fd(void *const *buffer, int size, int fd) — делает то же самое, что и предыдущая, только вместо выделения памяти под строки через malloc пишет инфу напрямую в файл.
Для каждой функции, вошедшей в стек вызовов, backtrace_symbols возвращает строку следующего вида:
./prog(_Z6myfunci+0x1a) [0x8048840]
где: prog — имя бинарника
_Z6myfunci — закодированное имя функции
0x1a — смещение внутри функции
0x8048840 — адрес функции
Найти более подробную информацию, а также пример их использования можно в man backtrace. Хочу лишь заметить, что для того, чтобы backtrace_symbols корректно отработала, компилировать программу надо с опцией -rdynamic. Это связано с тем, что информацию об имени функции backtrace_symbols берет из таблицы динамической линковки. А по умолчанию туда попадают только функции, подгружаемые из динамических библиотек. Для принудительного добавления всех функций в эту таблицу и нужен вышеупомянутый ключ.
Недостатком функции backtrace_symbols является то, что результат своей работы она представляет в виде текста. Т.е. если мы захотим произвести какие-либо манипуляции, например, с именем функции, то придется парсить эту строку. Опять не по-джедайски! Зачем это надо, будет понятно чуть позже.
Тут на помощь нам приходит функция dladdr. Собственно именно ее и зовет backtrace_symbols внутри себя. Её сигнатура очень проста — на вход подаем адрес функции, а на выходе получаем структуру типа Dl_info:
int dladdr(void *addr, Dl_info *info);
В случае успешного исхода вызова dladdr в структуре будет лежать все те же данные, что и в случае с backtrace_symbols.
Ну что ж, почти отлично. Теперь у нас есть адреса возвратов и даже имена функций, хоть и в закодированном формате (о решении этот вопроса поговорим чуть позже). Посмотрим, какую информацию можно еще вытащить. Может имя файла с исходным кодом и даже адрес строки где находится функция? Реально, хоть и придется заморочиться!
В принципе, тех данных, которые у нас уже есть, достаточно. Имея адрес, можно всегда выяснить номер строки, который породил вызов функции. Самый простой способ — использовать команду list отладчика gdb. Если у вас есть та же версия программы, собранная с дебагом, то list *<адрес> — покажет вам номер строки. А если же у вас рядом еще и исходники лежат, то вы «о чудо!» эту строку увидите.
Но идея хранить две версии программы (с дебагом и без) не соответствует джедайским стремлениям к идеальному, и поэтому я решил изучить strip. Я давно знал, что он умеет хранить бинарные файлы и отладочную информацию отдельно. Оказалось все довольно просто:
Теперь мы можем:
Но, если же есть желание видеть не только номера строк, но и сам исходный код, но при этом не хотите его заливать клиенту (вполне законное желание для коммерческого ПО), можно воспользоваться gdbserver, который позволяет отлаживать программу удаленно. Для этого нужно:
Напоследок скажу пару слов о формате записи имен функций. В двух словах подобное искажения имен функций необходимо компоновщику, чтобы решать коллизии именования. Ну, или еще проще, если в программе существуют две функции с одинаковым именем, но различными параметрами (перегруженные), то компоновщику необходимо точно знать с какой из них работать. Для этого компилятор и кодирует имя функции по особому алгоритму, назначая ей новое уникальное имя. По-английский это процесс называется mangling, а обратный ему – demangling.
Для решения задачи преобразования закодированного имени функции в оригинальный формат можно опять воспользоваться gcc-шным расширением:
char* abi::__cxa_demangle(const char* mangled_name, char* output_buffer, size_t* length, int* status)
Эта функция, принимая на вход закодированное имя функции и буфер, на выходе выдает раскодированное имя. Пример ее использования можно найти здесь.
Самым забавным способом получения нужной информации оказалось просто спросить её у gdb. Благо последний позволяет нам это сделать (пример функции взят отсюда [1]).
Все что нам нужно будет сделать, это вызвать функцию print_trace и, вуаля, стек вызовов распечатается в stdout. В принципе, вариант работающий, но очень медленный и требующий установки gdb.
Вот и все.
Приятной отладки!
Автор: Bandidasita
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmnoe-obespechenie/7977
Ссылки в тексте:
[1] отсюда: http://stackoverflow.com/questions/4636456/stack-trace-for-c-using-gcc
Нажмите здесь для печати.