- Stack Trace Класс
- Определение
- Примеры
- Комментарии
- Конструкторы
- Свойства
- Методы
- Что такое stack trace, и как с его помощью находить ошибки при разработке приложений?
- 1 ответ 1
- Windows C++ stack trace from a running app
- 3 Answers 3
- Limited stack trace in Process Explorer
- 1 Answer 1
- Stack Trace в C++ или велосипедирование, уровень «Быдлокод»
- DISCLAMER
- Вступление
- Смотрим по сторонам
- Реализация
Stack Trace Класс
Определение
Представляет трассировку стека — упорядоченный набор из одного или нескольких кадров стека. Represents a stack trace, which is an ordered collection of one or more stack frames.
Примеры
В следующем консольном приложении показано, как создать простой объект StackTrace и выполнить итерацию по его кадрам для получения сведений об отладке и диагностике. The following console application demonstrates how to create a simple StackTrace and iterate through its frames to obtain debugging and diagnostic information.
Комментарии
StackTrace информация будет наиболее информативной с отладочными конфигурациями сборки. StackTrace information will be most informative with Debug build configurations. По умолчанию отладочные сборки содержат отладочные символы, а сборки выпуска — нет. By default, Debug builds include debug symbols, while Release builds do not. Отладочные символы содержат большую часть сведений о файле, имени метода, номере строки и столбцах, используемых при построении StackFrame и StackTrace объектах. The debug symbols contain most of the file, method name, line number, and column information used in constructing StackFrame and StackTrace objects.
StackTrace может не сообщать о количестве вызовов методов, как ожидалось, из-за преобразований кода, происходящих во время оптимизации. StackTrace might not report as many method calls as expected, due to code transformations that occur during optimization.
Конструкторы
Инициализирует новый экземпляр класса StackTrace из фрагмента вызывающего оператора. Initializes a new instance of the StackTrace class from the caller’s frame.
Инициализирует новый экземпляр класса StackTrace из фрагмента вызывающего оператора и дополнительно может собирать сведения об источнике. Initializes a new instance of the StackTrace class from the caller’s frame, optionally capturing source information.
Инициализирует новый экземпляр класса StackTrace с использованием предоставленного объекта исключения. Initializes a new instance of the StackTrace class using the provided exception object.
Инициализирует новый экземпляр класса StackTrace с использованием предоставленного объекта исключения и дополнительно может собирать сведения об источнике. Initializes a new instance of the StackTrace class, using the provided exception object and optionally capturing source information.
Инициализирует новый экземпляр класса StackTrace с использованием предоставленного объекта, пропуская указанное число фрагментов. Initializes a new instance of the StackTrace class using the provided exception object and skipping the specified number of frames.
Инициализирует новый экземпляр класса StackTrace с использованием предоставленного объекта исключения, пропуская заданное число фрагментов и дополнительно собирая сведения об источнике. Initializes a new instance of the StackTrace class using the provided exception object, skipping the specified number of frames and optionally capturing source information.
Инициализирует новый экземпляр класса StackTrace из фрагмента вызывающего оператора, пропуская заданное число фрагментов. Initializes a new instance of the StackTrace class from the caller’s frame, skipping the specified number of frames.
Инициализирует новый экземпляр класса StackTrace из фрагмента вызывающего оператора, пропуская заданное число фрагментов и дополнительно собирая сведения об источнике. Initializes a new instance of the StackTrace class from the caller’s frame, skipping the specified number of frames and optionally capturing source information.
Инициализирует новый экземпляр класса StackTrace, содержащий один фрагмент. Initializes a new instance of the StackTrace class that contains a single frame.
Инициализирует новый экземпляр класса StackTrace для определенного потока и дополнительно может собирать сведения об источнике. Initializes a new instance of the StackTrace class for a specific thread, optionally capturing source information.
Не используйте эту перегрузку конструктора. Do not use this constructor overload.
Определяет значение по умолчанию для числа методов, которые нужно пропустить в трассировке стека. Defines the default for the number of methods to omit from the stack trace. Это поле является константой. This field is constant.
Свойства
Возвращает число фрагментов в трассировке стека. Gets the number of frames in the stack trace.
Методы
Определяет, равен ли указанный объект текущему объекту. Determines whether the specified object is equal to the current object.
(Унаследовано от Object)
Возвращает указанный кадр стека. Gets the specified stack frame.
Возвращает копию всех кадров стека в текущей трассировке стека. Returns a copy of all stack frames in the current stack trace.
Служит хэш-функцией по умолчанию. Serves as the default hash function.
(Унаследовано от Object)
Возвращает объект Type для текущего экземпляра. Gets the Type of the current instance.
(Унаследовано от Object)
Создает неполную копию текущего объекта Object. Creates a shallow copy of the current Object.
(Унаследовано от Object)
Создает доступное для чтения представление трассировки стека. Builds a readable representation of the stack trace.
Что такое stack trace, и как с его помощью находить ошибки при разработке приложений?
Иногда при запуске своего приложения я получаю подобную ошибку:
Мне сказали, что это называется «трассировкой стека» или «stack trace». Что такое трассировка? Какую полезную информацию об ошибке в разрабатываемой программе она содержит?
Немного по существу: довольно часто я вижу вопросы, в которых начинающие разработчики, получая ошибку, просто берут трассировки стека и какой-либо случайный фрагмент кода без понимания, что собой представляет трассировка и как с ней работать. Данный вопрос предназначен специально для начинающих разработчиков, которым может понадобиться помощь в понимании ценности трассировки стека вызовов.
1 ответ 1
Простыми словами, трассировка стека – это список методов, которые были вызваны до момента, когда в приложении произошло исключение.
Простой случай
В указанном примере мы можем точно определить, когда именно произошло исключение. Рассмотрим трассировку стека:
Это пример очень простой трассировки. Если пойти по списку строк вида «at…» с самого начала, мы можем понять, где произошла ошибка. Мы смотрим на верхний вызов функции. В нашем случае, это:
Для отладки этого фрагмента открываем Book.java и смотрим, что находится на строке 16 :
Это означает то, что в приведенном фрагменте кода какая-то переменная (вероятно, title ) имеет значение null .
Пример цепочки исключений
Иногда приложения перехватывают исключение и выбрасывают его в виде другого исключения. Обычно это выглядит так:
Трассировка в этом случае может иметь следующий вид:
В этом случае разница состоит в атрибуте «Caused by» («Чем вызвано»). Иногда исключения могут иметь несколько секций «Caused by». Обычно необходимо найти исходную причину, которой оказывается в самой последней (нижней) секции «Caused by» трассировки. В нашем случае, это:
Аналогично, при подобном исключении необходимо обратиться к строке 22 книги Book.java , чтобы узнать, что вызвало данное исключение – NullPointerException .
Еще один пугающий пример с библиотечным кодом
Как правило, трассировка имеет гораздо более сложный вид, чем в рассмотренных выше случаях. Приведу пример (длинная трассировка, демонстрирующая несколько уровней цепочек исключений):
В этом примере приведен далеко не полный стек вызовов. Что вызывает здесь наибольший интерес, так это поиск функций из нашего кода – из пакета com.example.myproject . В предыдущем примере мы сначала хотели отыскать «первопричину», а именно:
Однако все вызовы методов в данном случае относятся к библиотечному коду. Поэтому мы перейдем к предыдущей секции «Caused by» и найдем первый вызов метода из нашего кода, а именно:
Windows C++ stack trace from a running app
I saw an app, an SVN Visual Studio plugin, that displayed a beautiful readable stack trace, when it crashed.
I would love to add that to my application. How do I provide that? No emailing of the information, just a visual display is enough.
3 Answers 3
The core of the necessary code is StackWalk64 . To get much from that, you’ll also want/need to get symbol names with SymGetSymFromAddr64 (which requires SymLoadModule64 ) and (probably) SymGetLineFromAddr64 and GetThreadContext . If the target was written in C++, you’ll probably also want to use UnDecorateSymbolName . Along with those you’ll need a few ancillaries like SymInitialize , SymCleanup , and probably SymSetOptions .
Here’s a fairly minimal demo. The stack dump it produces is more utilitarian than beautiful, but presumably with the basics of doing the stack trace, you can display the results as you see fit:
I modified Jerry Coffin’s code as you see below. Modifications:
A. I was always missing the most important frame of all: the line that actually triggered the exception. Turns out this was because the «do» loop was moving to the next frame at the top instead of at the bottom.
B. I removed the threading stuff.
C. I simplified a few bits.
You should cut the «WinMain()» function at the bottom and instead put the __try .. __except stuff in your own ‘main/WinMain’ function. Also replace ‘YourMessage’ with your own function to display a message box or email it or whatever.
The symbolic information is stored inside a .pdb file, not the .exe. You need to give your users your .pdb file, and you need to ensure that their process can find it. See the string inside the code below — replace this with a folder that will work on the users’ machine, or NULL — NULL means it will search in the process’s current working directory. The .pdb file must have the exact same name on the users’ computer as it had when you ran your compiler — to configure this to something different, see «Properties > Linker > Debugging > Generate Program Database File».
Limited stack trace in Process Explorer
I have a process running under Windows Server 2003 SP2. When I want to check stack trace of one of its threads it is always limited to 9 entries. Those entries are resolved correctly (I have PDBs in place) but list is just cut in middle.
Do you know of any limitation in Process Explorer?
1 Answer 1
I am assuming that you think the complete stack trace for this thread should have more than 9 entries. You don’t mention if 32 bit OS or 64 bit OS, but I will assume 32 bit OS and then cover 64 bit as an afterthought.
Sometimes when collecting a stack trace on 32 bit systems you cannot collect any items for the stack trace or you can only collect a limited amount of stack frame information even though you know the callstack is deeper. The reasons for this are:
Different calling conventions put data in different places on the stack, making it hard to walk the stack. I can think of 4 definitions, 3 in common use, one more exotic: cdecl, fastcall, stdcall, naked.
For release builds, the code optimizer may do away with the frame pointers using a technique known as Frame Pointer Omission (FPO). Without the FPO (and sometimes, even with the FPO data in a PDB file) you cannot successfully walk the callstack.
Hooks — any helper DLLs, anti-virus, debugging hooks, instrumented code, malware, etc, may mess up the callstack at somepoint because they’ve inserted their own stub code on the callstack and that small section may not be walkable by the stack walker.
Bytecode virtual machines. Depending upon how the virtual machine is written, the VM may place trampolines on the callstack to aid its execution. These will make the stack hard to walk successfully.
Because of the variety of calling conventions on 32 bit Windows (from both Microsoft and other vendors) it is hard to work out what to expect when you move from one frame to another.
For 64 bit systems there is one calling convention specified. That makes life a lot easier. That said, you still have the issues of helper DLLs and hooks doing their own thing with the stack and that may still cause you problems when walking the stack.
I doubt there is a limitation in Process Explorer. I think the issue is just that walking the callstack for that thread is problematic because of one of the reasons I’ve listed above.
Stack Trace в C++ или велосипедирование, уровень «Быдлокод»
DISCLAMER
Статья является шуточной, но с долей правды (программирование, же). Данная статья также содержит код, который может смертельно навредить вашему зрению. Читайте на ваш риск.
Вступление
Исключение
Исключения — это очень мощная система обработки исключительных ситуаций, возникающих в программе. Но если исключение не было обработано — то оно роняет программу через std::terminate. Поэтому в хорошо написанных программах, исключение которое не было обработано зачастую означает баг в программе, который надо исправлять.
Данный вид ошибок является самым информативным, так как метод исключения what() выводится в stderr автоматически при падении программы.
Assert
Отключаемый метод контроля правильности использования функций. Отключение предоставляется в качестве инструмента увеличения производительности в функциях, в которых каждая наносекунда на счету. Если программа упала по assertу, значит программист где-то накосячил при использовании интерфейса какого то модуля. Но вполне возможно, что он просто не предусмотрел каких-то критических значений и по этой причине вышел из условий assertа.
Данный вид ошибок является не самым информативным, но при падении, выводит условие, которое было нарушено.
SIGSEGV
Вы, как профессионал своего дела разыменовали нулевой указатель и радостно записали в него какое-то значение. Программа не особо сопротивляясь упала.
Такое падение не сопровождается никакими сообщениями и является наверное самым не информативным и представленных, но оно есть и исключать его ни в коем случае нельзя.
Все виды ошибок, вне зависимости от их информативности, не очень помогают определить по какой же причине она появилась. В рамках этой статьи я попробую показать, что у меня получилось в порыве получить хоть какой-то stack trace во время отлова ошибок.
Смотрим по сторонам
Для начала надо понять, каким образом вообще отслеживать вызовы функций. Гуглинг выдал крайне неутешительные результаты. Очевидно, что кроссплатформенного решения нет. Под Linux и Mac OS есть заголовочный файл execinfo.h с помощью которого можно получить связный список стека вызовов. Под Windows есть функция WinAPI CaptureStackBackTrace, которая позволяет прогуляться по стеку и получить вызовы из фреймов. Но мы пойдем путем С++. Не будем использовать платформозависимые функции.
Данные будем хранить в обычном стеке. Для заталкивания и выталкивания функций будем использовать объект, который будет создаваться во время вызова функции. Преимущества такого подхода в том, что даже если будет вызвано исключение, этот объект удалиться.
А какие конкретно нам нужны данные? Ну для красоты конечно было бы не плохо иметь файл, строку и имя функции. Так же еще было бы не плохо иметь аргументы этой функции, что бы можно было конкретизировать вызываемую функцию при перегрузке.
Но какой использовать интерфейс? Как написать более менее красивый код и при этом получить требуемую функциональность.
Единственное решение которое я смог найти — это макросы (возможно, это все также можно как-то реализовать через шаблоны, но я с шаблонами знаком крайне поверхностно и поэтому делаю так как умею).
Реализация
Для начала реализуем синглетон, который будет использоваться для работы со стеком. В качестве интерфейса пользователя реализуем только метод для получения строкового представления stack traceа.
Нет возможности использовать std::stack, так как для того, что бы получить все элементы для вывода пришлось бы копировать весь контейнер.
Из проблем данного класса — полная потоковая небезопасность. Но с этим мы разберемся позже, а сейчас PoC.
Теперь реализуем класс, который будет регистрировать и удалять вызов функций.
Довольно нетривиальный код не так ли? Опять же, данный «регистратор» не учитывает многопоточность.
Теперь попробуем накидать небольшой пример, что бы проверить работоспособность такого Франкенштейна.
Рисунок 3.1 — «Оно живое!!»
Отлично! Но надо же как-то упаковать вызов CallHolder, а то не красиво как-то получается ручками вызывать и два раза прописывать название метода.
Для реализаций функций и методов получился такой вот макрос:
Теперь нашего Франкенштейна можно модифицировать и получить что-то вроде этого. Уже более похоже на «обычный» код:
Результат выполнения ровно такой же, как и ранее. Но в данном подходе есть явная проблема. Пропадает открывающая фигурная скобка, которую скрывает макрос. Это усложняет чтение кода. Хотя люди, которые придерживаются идеологии с открывающей фигурной скобкой в строке с заголовком не сочтут это сильным минусом. Более сильный минус, что среда разработки, которой я пользуюсь не умеет работать с такими изворотливыми случаями и считает только фигурные скобки вне макросов.
Но мы отвлеклись от нашей вакханалии. Что же делать, если у нас класс? Ну если реализация вне класса — то ничего. Пример:
Рисунок 3.2 — Вывод из класса
А что, если вы пишете реализацию прямо в объявлении класса? Тогда требуется другой макрос:
Но у такого подхода есть проблема. В нем надо отдельно указывать имя класса, что не очень хорошо. Это можно обскакать, если мы используем С++11. Я использую найденное на stack overflow решение. Это type_name (). Где type_name это
Часть с модификаторами закомментирована по той причине, что результат обработки (*this) тогда будет в конце иметь знак ссылки — амперсанд (&).
Хитрожопый макрос выглядит так:
Подредактируем нашего франка и посмотрим на результат:
Рисунок 3.3 — Объявленный внутри метод класса
Хорошо, но что там с информативностью? Каким образом можно получить хоть какую-нибудь полезную информацию при падении. Ведь сейчас при возникновении того же Seg Fault все просто упадет. Ну для начала реализуем свой int main, который будет ловить ошибки. В заголовке объявляем:
В cpp реализуем наш «безопасный» main, который уже вызовет safe_main.
Думаю стоит объясниться. Функцией signal мы устанавливаем обработчик, который вызовется при появлении сигналов SIGSEGV, SIGTERM и SIGABRT. В котором уже будет выведен в stderr stack trace. (Последний требуется для assert).
Попробуем сломать программу SIGSEGV. Опять изменим наш «тестовый стенд»:
Рисунок 3.4 — Работа безопасного main
Но как обстоят дела с исключениями? Ведь если вызывать исключение — то оно просто поразрушает все имеющиеся CallHolder и в stack trace мы не получим ничего обстоятельного. Для этого создаем собственный THROW макрос, который бы получал stack trace в момент выброса исключения:
Так же модифицируем немного наш «тестовый стенд»:
И получаем результат:
Рисунок 3.5 — THROW не прощает
Хорошо. Мы добились полного базового функционала, но что там с многопоточностью? Будем ли мы с ней что-то делать?
Ну по крайней мере попробуем!
Для начала редактируем StackTracer, что бы он начал работать с разными потоками:
Аналогично меняем CallHolder, что бы в него передавался thread_id:
Ну и модифицируем немного макросы:
Тестируем. Подготовим такой «стенд»:
И попробуем запустить:
Рисунок 3.6 — Смерть наступила в 1:10 по московскому времени
Вот мы и получили многопоточный stack trace. Эксперимент окончен, подопытный мертв. Из очевидных проблем данной реализации
- Мы не можем получить вызовы из библиотек не написанных нами;
- Дополнительные накладные расходы на каждый вызов функции.