Зачем включать стектрейс в стандарт C++?

Алексей Горгуров, преподаватель МТУСИ, старший разработчик НПЛ «Медоптика» и Software Developer в Synchro Software

Российская рабочая группа по стандартизации C++ собирает предложения разработчиков, чтобы донести их до Международного комитета по стандартизации. Все нововведения призваны сделать язык более логичным и упростить его использование. Сейчас на рассмотрении у комитета находится предложение о включении в стандарт стектрейса. О том, зачем это нужно, рассказываем ниже.

Иногда при работе над большим проектом возникает необходимость узнать, как мы попали в ту или иную функцию. Например, если интересующая нас функция вызывается только при обнаружении ошибки, то нам захочется увидеть последовательность вызовов, приводящих к проблеме. Для этой цели можно было бы использовать отладчик — остановить исполнение в интересующем месте и распечатать стек. Однако во многих сценариях этот способ неприменим: в случаях, если нужно получить стек вызовов с устройства, где нет отладчика, или с приложения, не терпящего вмешательства извне. В подобных случаях необходимо получать стек прямо в процессе исполнения программы. Это можно сделать разными способами — в зависимости от потребностей, компилятора и от платформы.

При работе на Linux, и если в стеке не нужно отображать исходные файлы и номера строк — можно воспользоваться вызовами backtrace и backtrace_symbols из библиотеки glibc. Результирующий стек может выглядеть примерно так:

backtrace() returned 6 addresses
./prog(myfunc3+0x5c) [0x80487f0]
./prog(_ZN2my3fooIdbEEvT_T0_+0x21) [0x8048894]
./prog(_ZN3clsIdE3barEv+0x1a) [0x804888d]
./prog(myfunc+0x1a) [0x804888d]
./prog(main+0x65) [0x80488fb]
/lib/libc.so.6(__libc_start_main) [0x8048711]

В этом стеке выражение в круглых скобках означает адрес исполняющейся в данный момент процессорной инструкции (myfunc+0x1a означает, что к адресу функции myfunc прибавляется смещение 0x1a). Страшная строчка _ZN3clsIdE3barEv — это тоже имя функции. Так как backtrace() предназначен для работы в первую очередь с кодом, написанном на C, то, если вы пишете на C++, имена ваших функций будут выводиться в некрасивом (мангленном) виде. Воспользовавшись специальными стронними инструментами, можно получить из строчки _ZN3clsIdE3barEv читаемое имя функции void cls<double>::bar().

Если мы захотим получить похожую информацию на платформе Windows при работе с компилятором MSVC, то можно использовать вызовы CaptureStackBackTrace и SymFromAddr из библиотеки kernel32.dll, или воспользоваться специальной библиотекой DbgHelp от Microsoft. И тут мы столкнёмся как со старыми проблемами нечитаемых мангленных имён, так и с новой проблемой — многие из вышеперечисленных функций неверно работают в многопоточной среде.

Если же нам ко всем прочему захочется получать ещё и имена исходных файлов в стектрейсе — придётся очень сильно постараться и научиться читать отладочную информацию, генерируемую компилятором вместе с кодом. Существует несколько форматов такой отладочной информации. GCC и Clang используют формат DWARF, а MSVC, — компилятор С++, использующийся в Visual Studio, — использует формат PDB. Чтение такой отладочной информации — непростой процесс, поэтому если требуется отображать информацию по номерам строк — проще воспользоваться уже готовым решением. И тут нас будут ждать очередные проблемы.

Так, библиотека libbacktrace умеет работать с отладочной информацией в формате DWARF, а, кроме того, доступна не только на Linux, но и на MacOS, а при компиляции приложения с помощью GCC или Clang сможет показать стек даже на Windows. Однако она не умеет работать с отладочной информацией, сгенерированной компилятором MSVC.

Обилие возможных форматов, компиляторов и платформ, необходимость подключать различные сторонние библиотеки, порождает сложности при желании включить эту функциональность в свой проект. Решить проблему помогло бы включение классов для получения стека вызовов в стандарт C++. Такое предложение поступило на сайт Национальной рабочей группы по стандартизации C++. Согласно предложению, для распечатки стека достаточно будет написать:

std::stacktrace s;
std::cerr << s;

На выходе получим что-то вроде:

0# myfunc3() at /path/to/source/file.cpp:70
1# void my::foo<double, bool>(double, bool) at /path/to/source/my.cpp:10
2# void cls<double>::bar() at /path/to/source/some_bar.cpp:77
3# myfunc() at /path/to/source/file.cpp:50
4# main at /path/to/main.cpp:93
5# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.
6# _start

Теперь не нужно ограничиваться определенной платформой или зависеть от компилятора — если предложение будет принято, любой компилятор, поддерживающий стандарт, сможет скомпилировать этот код. Не нужно будет волноваться о формате отладочной информации — компилятор позаботится и об этом. Исчезнет зависимость от сторонних библиотек.

Например, чтобы вывести стек при фатальной ошибке, можно будет написать свой обработчик std::terminate:

#include <cstdlib> 
#include <iostream> 
#include <stacktrace> 

void my_terminate_handler() {
   std::cerr << std::stacktrace();
   std::abort();
}

и зарегистрировать его:

std::set_terminate(&my_terminate_handler);

Кроме этого примера применений может быть масса — например, для вывода стека при обработке исключений и assert’ов или в профилировщиках для сбора информации о местах, где выделяется память.

Реализацию предлагаемой библиотеки в стандарт можно найти в репозитории Boost.Stacktrace, само предложение можно почитать на сайте Рабочей Группы 21.

В настоящее время готовится небольшое расширение для уже имеющегося предложения, включающее в себя возможность компактно сохранять и переносить стектрейсы с одной машины на другую.

Подобрали три теста для вас:
— А здесь можно применить блокчейн?
Серверы для котиков: выберите лучшее решение для проекта и проверьте себя.
Сложный тест по C# — проверьте свои знания.

Также рекомендуем: