Stack trace что за файлы на андроиде
Иногда при запуске своего приложения я получаю подобную ошибку:
Мне сказали, что это называется «трассировкой стека» или «stack trace». Что такое трассировка? Какую полезную информацию об ошибке в разрабатываемой программе она содержит?
Немного по существу: довольно часто я вижу вопросы, в которых начинающие разработчики, получая ошибку, просто берут трассировки стека и какой-либо случайный фрагмент кода без понимания, что собой представляет трассировка и как с ней работать. Данный вопрос предназначен специально для начинающих разработчиков, которым может понадобиться помощь в понимании ценности трассировки стека вызовов.
3. Стек
Что такое Stack Trace вы уже знаете, а что же такое сам Stack (Стек)?
Стек — это структура хранения данных, в которую можно добавлять элементы и из которой можно забирать элементы. Причем брать элементы можно только с конца: сначала последний добавленный, потом — предпоследний, и т.д.
Само название Stack переводится с английского как «стопка» и очень похоже на стопку бумаги. Если вы положите на стопку бумаги листы 1, 2 и 3, взять вы их сможете только в обратном порядке: сначала третий, затем второй, а только затем первый.
В Java даже есть специальная коллекция с таким поведением и таким же названием — Stack. Этот класс в своем поведении очень похож на ArrayList и LinkedList . Однако у него есть еще методы, которые реализуют поведение стека:
Методы | Описание |
---|---|
Добавляет элемент obj в конец списка (наверх стопки) | |
Забирает элемент с верха стопки (высота стопки уменьшается) | |
Возвращает элемент с верха стопки (стопка не меняется) | |
Проверяет, не пуста ли коллекция | |
Ищет объект из коллекции, возвращает его index |
Код | Содержимое стека (вершина справа) |
---|
Стек используется в программировании довольно часто. Так что это полезная коллекция.
1 ответ 1
Простыми словами, трассировка стека – это список методов, которые были вызваны до момента, когда в приложении произошло исключение.
Простой случай
В указанном примере мы можем точно определить, когда именно произошло исключение. Рассмотрим трассировку стека:
Это пример очень простой трассировки. Если пойти по списку строк вида «at…» с самого начала, мы можем понять, где произошла ошибка. Мы смотрим на верхний вызов функции. В нашем случае, это:
Для отладки этого фрагмента открываем Book.java и смотрим, что находится на строке 16 :
Это означает то, что в приведенном фрагменте кода какая-то переменная (вероятно, title ) имеет значение null .
Пример цепочки исключений
Иногда приложения перехватывают исключение и выбрасывают его в виде другого исключения. Обычно это выглядит так:
Трассировка в этом случае может иметь следующий вид:
В этом случае разница состоит в атрибуте "Caused by" («Чем вызвано»). Иногда исключения могут иметь несколько секций "Caused by". Обычно необходимо найти исходную причину, которой оказывается в самой последней (нижней) секции "Caused by" трассировки. В нашем случае, это:
Аналогично, при подобном исключении необходимо обратиться к строке 22 книги Book.java , чтобы узнать, что вызвало данное исключение – NullPointerException .
Еще один пугающий пример с библиотечным кодом
Как правило, трассировка имеет гораздо более сложный вид, чем в рассмотренных выше случаях. Приведу пример (длинная трассировка, демонстрирующая несколько уровней цепочек исключений):
В этом примере приведен далеко не полный стек вызовов. Что вызывает здесь наибольший интерес, так это поиск функций из нашего кода – из пакета com.example.myproject . В предыдущем примере мы сначала хотели отыскать «первопричину», а именно:
Однако все вызовы методов в данном случае относятся к библиотечному коду. Поэтому мы перейдем к предыдущей секции «Caused by» и найдем первый вызов метода из нашего кода, а именно:
Аналогично предыдущим примерам, необходимо обратить внимание на MyEntityService.java , строка 59 : именно здесь появилась ошибка (в данном случае ситуация довольно очевидная, так как об ошибке сообщает SQLException , но в этом вопросе мы рассматриваем именно процедуру отладки с помощью трассировки).
Добрый день! Несколько дней назад столкнулся с небольшой проблемой в нашем проекте — в обработчике прерывания gdb неправильно выводил stack trace для Cortex-M. Поэтому в очередной раз полез выяснять, а какими способами можно получать stack trace для ARM? Какие флаги компиляции влияют на возможность трассировки стека на ARM? Как это реализовано в ядре Linux? По результатам исследований решил написать эту статью.
Разберем два основных метода трассировки стека в ядре Linux.
Stack unwind через фреймы
Начнем с простого подхода, который можно найти в ядре Линукс, но который на данный момент в GCC имеет статус deprecated.
Представим, что исполняется некая программа на стеке в ОЗУ, и в какой-то момент мы ее прерываем и хотим вывести стек вызовов. Пусть у нас есть указатель на текущую инструкцию, которая выполняется процессором (PC), а также текущий указатель на вершину стека (SP). Теперь, чтобы “прыгнуть” вверх по стеку к предыдущей функции, нужно понять, что же это была за функция и в какое место этой функции мы должны прыгнуть. В ARM для этой цели используется Link Register (LR),
The Link Register (LR) is register R14. It stores the return information for subroutines, function calls, and exceptions. On reset, the processor sets the LR value to 0xFFFFFFFF
Далее нам нужно идти по стеку вверх и загружать новые значения регистров LR со стека. Структура стекового фрейма для компилятора выглядит следующим образом:
Это описание взято из заголовочного файла GCC gcc/gcc/config/arm/arm.h.
Такой режим компилятора задается опцией -mapcs-frame. В описании опции есть упоминание про “Specifying -fomit-frame-pointer with this option causes the stack frames not to be generated for leaf functions.” Здесь под leaf-функциями понимаются те, которые не делают никаких вызовов других функций, поэтому их можно сделать чуть более легкими.
Также может возникнуть вопрос, что делать с ассемблерными функциями в этом случае. На самом деле, ничего хитрого — нужно вставлять специальные макросы. Из файла tools/objtool/Documentation/stack-validation.txt в ядре Linux:
Each callable function must be annotated as such with the ELF
function type. In asm code, this is typically done using the
ENTRY/ENDPROC macros.
Но в этом же документе обсуждается, что это является и очевидным минусом такого подхода. Утилита objtool проверяет, все ли функции в ядре написаны в нужном формате для трассировки стека.
Ниже приведена функция раскручивания стека из ядра Linux:
Но тут я хочу отметить строчку с defined(CONFIG_ARM_UNWIND) . Она намекает, что в ядре Линукс используется и другая реализация unwind_frame, и о ней мы поговорим чуть позже.
Опция -mapcs-frame верна только для набора инструкций ARM. Но известно, что у микроконтроллеров ARM есть и другой набор инструкций — Thumb (Thumb-1 и Thumb-2, если быть точнее), он используется в основном для серии Cortex-M. Чтобы включить генерацию фреймов для режима Thumb следует использовать флаги -mtpcs-frame и -mtpcs-leaf-frame. По сути, это аналог -mapcs-frame. Интересно, что эти опции на данный момент работают только для Cortex-M0/M1. Я какое-то время не мог разобраться, почему не получается скомпилировать нужный образ для Cortex-M3/M4/…. После того, как перечитал все опции gcc для ARM и поискал в интернете, понял, что это, вероятно, баг. Поэтому полез непосредственно в сами исходники компилятора arm-none-eabi-gcc. После изучения того, как компилятор генерируется фреймы для ARM, Thumb-1 и Thumb-2, я пришел к выводу, что они обошли стороной Thumb-2, т.е на данный момент фреймы генерируются только для Thumb-1 и ARM. После создания баги, разработчики GCC пояснили, что стандарт для ARM уже менялся несколько раз и эти флаги сильно устарели, но по некоторым причинам все они до сих пор существуют в компиляторе. Ниже приведен дизассемблер функции, для которой сгененирован фрейм.
Для сравнения, дизассемблер той же функции для инструкций ARM
На первый взгляд может показаться, что это совсем разные вещи. Но на самом деле фреймы абсолютно одинаковые, дело в том, что в Thumb режиме инструкция push разрешает укладывать на стек только low регистры (r0 — r7) и регистр lr. Для всех остальных регистров это приходится делать в два этапа через инструкции mov и str как в примере выше.
Stack unwind через исключения
Альтернативным подходом является раскручивание стека, основанное на стандарте “Exception Handling ABI for the ARM Architecture” (EHABI). По сути главным примером использования этого стандарта является обработка исключений в таких языках как С++. Информацию подготовленную компилятором для обработки исключений можно использовать также и для трассировки стека. Включается такой режим опцией GCC -fexceptions (или -funwind-frames).
Посмотрим подробнее на то, как это делается. Для начала, этот документ (EHABI) накладывает определенные требования на компилятор по генерации вспомогательных таблиц .ARM.exidx и .ARM.extab. Вот так эта секция .ARM.exidx определяется в исходниках ядра Linux. Из файла arch/arm/kernel/vmlinux.lds.h:
Стандарт “Exception Handling ABI for the ARM Architecture” определяет каждый элемент таблицы .ARM.exidx как следующую структуру:
Первый элемент — это смещение относительно начала функции, а второй элемент — это адрес в таблице инструкций, которые нужно специальным образом интерпретировать для того, чтобы раскрутить стек дальше. Другими словами, каждый элемент этой таблицы это просто последовательность слов и полуслов, которые и представляют собой последовательность инструкций. В первом слове указано то кол-во инструкций, которое нужно выполнить, чтобы раскрутить стек до следующего фрейма.
Описание этих инструкций приведено в уже упомянутом стандарте EHABI:
Далее, основная реализация этого интерпретатора в Linux находится в файле arch/arm/kernel/unwind.c
Эта реализация функции unwind_frame, которая используется, если включена опция CONFIG_ARM_UNWIND. Комментарии с объяснениями на русском я вставил прямо в исходный текст.
Ниже представлен пример того, как выглядит элемент таблицы .ARM.exidx для функции kernel_start в Embox:
А вот ее дизассемблер:
Теперь давайте посмотрим, сколько памяти “отъедает” сборка с флагом -funwind-frames
Для эксперимента я скомпилировал Embox для платформы STM32F4-Discovery. Вот результаты objdump:
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0005a600 08000000 08000000 00004000 2**14
CONTENTS, ALLOC, LOAD, CODE
1 .ARM.exidx 00003fd8 0805a600 0805a600 0005e600 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .ARM.extab 000049d0 0805e5d8 0805e5d8 000625d8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .rodata 0003e380 08062fc0 08062fc0 00066fc0 2**5
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00058b1c 08000000 08000000 00004000 2**14
CONTENTS, ALLOC, LOAD, CODE
1 .ARM.exidx 00000008 08058b1c 08058b1c 0005cb1c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .rodata 0003e380 08058b40 08058b40 0005cb40 2**5
Несложно подсчитать, что секции .ARM.exidx и .ARM.extab занимают примерно 1/10 часть от размера .text. После этого я собрал образ побольше — для ARM Integrator CP на базе ARM9, и там эти секции составили 1/12 от размера секции .text. Но ясно, что такое соотношение может меняться от проекта к проекту. Также выяснилось, что размер образа, который добавляет флаг -macps-frame меньше, чем вариант с исключениями (что ожидаемо). Так, например, при размере секции .text в 600 Кб, суммарный размер .ARM.exidx + .ARM.extab составлял 50 Кб, а размер дополнительного кода c флагом -mapcs-frame всего 10 Кб. Но если мы посмотрим выше, какой большой пролог генерировался для Cortex-M1 (помните, через mov/str?), то становится понятно, что в этом случае разницы практически не будет, а значит для Thumb-режима использование -mtpcs-frame вряд ли имеет хоть какой-то смысл.
А нужен ли такой stack trace сейчас для ARM? Какие альтернативы?
Третьим подходом является трассировка стека при помощи отладчика. Похоже, многие ОС для работы с микроконтроллерами FreeRTOS, NuttX на данный момент предполагают именно этот вариант трассировки или предлагают смотреть дизассемблер.
В итоге мы пришли к выводу, что трассировка стека для армов в run time фактически нигде не применяется. Вероятно, это следствие стремления сделать наиболее эффективный код во время работы, а действия по отладке (к которым относится и раскрутка стека) вынести в оффлайн. С другой стороны, если в ОС уже используется код на C++, то вполне можно воспользоваться реализацией трассировки через .ARM.exidx.
Ну и да, проблема с неправильным выводом стека в прерывании в Embox, решилась очень просто, оказалось достаточно сохранить на стек регистр LR.
Практика показала, что хардкорные расшифровки с наших докладов хорошо заходят, так что мы решили продолжать. Сегодня у нас в меню смесь из подходов к поиску и анализу ошибок и крэшей, приправленная щепоткой полезных инструментов, подготовленная на основе доклада Андрея Паньгина aka apangin из Одноклассников на одном из JUG'ов (это была допиленная версия его доклада с JPoint 2016). В без семи минут двухчасовом докладе Андрей подробно рассказывает о стек-трейсах и хип-дампах.
Пост получился просто огромный, так что мы разбили его на две части. Сейчас вы читаете первую часть, вторая часть лежит здесь.
Сегодня я буду рассказывать про стек-трейсы и хип-дампы — тему, с одной стороны, известную каждому, с другой — позволяющую постоянно открывать что-то новое (я даже багу нашел в JVM, пока готовил эту тему).
Когда я делал тренировочный прогон этого доклада у нас в офисе, один из коллег спросил: «Все это очень интересно, но на практике это кому-нибудь вообще полезно?» После этого разговора первым слайдом в свою презентацию я добавил страницу с вопросами по теме на StackOverflow. Так что это актуально.
Сам я работаю ведущим программистом в Одноклассниках. И так сложилось, что зачастую мне приходится работать с внутренностями Java — тюнить ее, искать баги, дергать что-то через системные классы (порой не совсем легальными способами). Оттуда я и почерпнул большую часть информации, которую сегодня хотел вам представить. Конечно, в этом мне очень помог мой предыдущий опыт: я 6 лет работал в Sun Microsystems, занимался непосредственно разработкой виртуальной Java-машины. Так что теперь я знаю эту тему как изнутри JVM, так и со стороны пользователя-разработчика.
Стек-трейсы
Стек-трейсы exception
Когда начинающий разработчик пишет свой «Hello world!», у него выскакивает эксепшн и ему демонстрируется стек-трейс, где произошла эта ошибка. Так что какие-то представления о стек-трейсах есть у большинства.
Перейдем сразу к примерам.
Я написал небольшую программку, которая в цикле 100 миллионов раз производит такой эксперимент: создает массив из 10 случайных элементов типа long и проверяет, сортированный он получился или нет.
По сути он считает вероятность получения сортированного массива, которая приблизительно равна 1/n! . Как это часто бывает, в программке ошиблись на единичку:
Что произойдет? Эксепшн, выход за пределы массива.
Давайте разбираться, в чем дело. У нас в консоль выводится:
но стек-трейсов никаких нет. Куда делись?
Теперь попробуем запустить пример. Получаем все то же самое, только все стек-трейсы на месте.
Подобная оптимизация работает для всех неявных эксепшенов, которые бросает JVM: выход за границы массива, разыменование нулевого указателя и т.д.
Раз оптимизацию придумали, значит она зачем-то нужна? Понятно, что программисту удобнее, когда стек-трейсы есть.
Давайте измерим, сколько «стоит» у нас создание эксепшена (сравним с каким-нибудь простым Java-объектом, вроде Date).
С помощью JMH напишем простенькую бенчмарку и измерим, сколько наносекунд занимают обе операции.
Оказывается, создать эксепшн в 150 раз дороже, чем обычный объект.
И тут не все так просто. Для виртуальной машины эксепшн не отличается от любого другого объекта, но разгадка кроется в том, что практически все конструкторы эксепшн так или иначе сводятся к вызову метода fillInStackTrace, который заполняет стек-трейс этого эксепшена. Именно заполнение стек-трейса отнимает время.
Этот метод в свою очередь нативный, падает в VM рантайм и там гуляет по стеку, собирает все фреймы.
Метод fillInStackTrace публичный, не final. Давайте его просто переопределим:
Теперь создание обычного объекта и эксепшена без стек-трейса отнимают одинаковое время.
Есть и другой способ создать эксепшн без стек-трейса. Начиная с Java 7, у Throwable и у Exception есть protected-конструктор с дополнительным параметром writableStackTrace:
Если туда передать false, то стек-трейс генерироваться не будет, и создание эксепшена будет очень быстрым.
Зачем нужны эксепшены без стек-трейсов? К примеру, если эксепшн используется в коде в качестве способа быстро выбраться из цикла. Конечно, лучше так не делать, но бывают случаи, когда это действительно дает прирост производительности.
А сколько стоит бросить эксепшн?
Рассмотрим разные случаи: когда он бросается и ловится в одном методе, а также ситуации с разной глубиной стека.
Вот, что дают измерения:
Т.е. если у нас глубина небольшая (эксепшн ловится в том же фрейме или фреймом выше — глубина 0 или 1), эксепшн ничего не стоит. Но как только глубина стека становится большой, затраты совсем другого порядка. При этом наблюдается четкая линейная зависимость: «стоимость» исключения почти линейно зависит от глубины стека.
Дорого стоит не только получение стек-трейса, но и дальнейшие манипуляции — распечатка, отправка по сети, запись, — все, для чего используется метод getStackTrace, который переводит сохраненный стек-трейс в объекты Java.
Видно, что преобразование стек-трейса в 10 раз «дороже» его получения:
Почему это происходит?
Вот метод getStackTrace в исходниках JDK:
Сначала через вызов нативного метода мы узнаем глубину стека, потом в цикле до этой глубины вызываем нативный метод, чтобы получить очередной фрейм и сконвертировать его в объект StackTraceElement (это нормальный объект Java с кучей полей). Мало того, что это долго, процедура отнимает много памяти.
Более того, в Java 9 этот объект дополнен новыми полями (в связи с известным проектом модуляризации) — теперь каждому фрейму приписывается отметка о том, из какого он модуля.
Привет тем, кто парсит эксепшены с помощью регулярных выражений. Готовьтесь к сюрпризам в Java 9 — появятся еще и модули.
Давайте подведем итоги
-
создание самого объекта эксепшн — дешевое;
Пара советов:
-
отключайте на продакшене оптимизацию, возможно, это сэкономит много времени на отладке:
Стек-трейсы в тред дампах
Чтобы узнать, что же делает программа, проще всего взять тред дамп, например, утилитой jstack.
Фрагменты вывода этой утилиты:
Что здесь видно? Какие есть потоки, в каком они состоянии и их текущий стек.
Более того, если потоки захватили какие-то локи, ожидают входа в synchronized-секцию или взятия ReentrantLock, это также будет отражено в стек-трейсе.
Порой полезным оказывается малоизвестный идентификатор:
Он напрямую связан с ID потока в операционной системе. Например, если вы смотрите программой top в Linux, какие треды у вас больше всего едят CPU, pid потока — это и есть тот самый nid, который демонстрируется в тред дампе. Можно тут же найти, какому Java-потоку он соответствует.
В случае с мониторами (с synchronized-объектами) прямо в тред дампе будет написано, какой тред и какие мониторы держит, кто пытается их захватить.
В случае с ReentrantLock это, к сожалению, не так. Здесь видно, как Thread 1 пытается захватить некий ReentrantLock, но при этом не видно, кто этот лок держит. На этот случай в VM есть опция:
Если мы запустим то же самое с PrintConcurrentLocks, в тред дампе увидим и ReentrantLock.
Здесь указан тот самый id лока. Видно, что его захватил Thread 2.
Если опция такая хорошая, почему бы ее не сделать «по умолчанию»?
Она тоже чего-то стоит. Чтобы напечатать информацию о том, какой поток какие ReentrantLock’и держит, JVM пробегает весь Java heap, ищет там все ReentrantLock’и, сопоставляет их с тредами и только потом выводит эту информацию (у треда нет информации о том, какие локи он захватил; информация есть только в обратную сторону — какой лок связан с каким тредом).
В указанном примере по названиям потоков (Thread 1 / Thread 2) непонятно, к чему они относятся. Мой совет из практики: если у вас происходит какая-то длинная операция, например, сервер обрабатывает клиентские запросы или, наоборот, клиент ходит к нескольким серверам, выставляйте треду понятное имя (как в случае ниже — прямо IP того сервера, к которому клиент сейчас идет). И тогда в дампе потока сразу будет видно, ответа от какого сервера он сейчас ждет.
Хватит теории. Давайте опять к практике. Этот пример я уже не раз приводил.
Запускаем программку 3 раза подряд. 2 раза она выводит сумму чисел от 0 до 100 (не включая 100), третий — не хочет. Давайте смотреть тред дампы:
Первый поток оказывается RUNNABLE, выполняет наш reduce. Но смотрите, какой интересный момент: Thread.State вроде как RUNNABLE, но при этом написано, что поток in Object.wait().
Мне тоже это было не понятно. Я даже хотел сообщить о баге, но оказывается, такая бага заведена много лет назад и закрыта с формулировкой: «not an issue, will not fix».
В этой программке действительно есть дедлок. Его причина — инициализация классов.
Выражение выполняется в статическом инициализаторе класса ParallelSum:
Но поскольку стрим параллельный, исполнение происходит в отдельных потоках ForkJoinPool, из которых вызывается тело лямбды:
Код лямбды записан Java-компилятором прямо в классе ParallelSum в виде приватного метода. Получается, что из ForkJoinPool мы пытаемся обратиться к классу ParallelSum, который в данный момент находится на этапе инициализации. Поэтому потоки начинают ждать, когда же закончится инициализация класса, а она не может закончиться, поскольку ожидает вычисления этой самой свертки. Дедлок.
Почему вначале сумма считалась? Просто повезло. У нас небольшое количество элементов суммируется, и иногда все исполняется в одном потоке (другой поток просто не успевает).
Но почему же тогда поток в стек-трейсе RUNNABLE? Если почитать документацию к Thread.State, станет понятно, что никакого другого состояния здесь быть не может. Не может быть состояния BLOCKED, поскольку поток не заблокирован на Java-мониторе, нет никакой synchronized-секции, и не может быть состояния WAITING, потому что здесь нет никаких вызовов Object.wait(). Синхронизация происходит на внутреннем объекте виртуальной машины, который, вообще говоря, даже не обязан быть Java-объектом.
Стек-трейс при логировании
Представьте себе ситуацию: в куче мест в нашем приложении что-то логируется. Было бы полезно узнать, из какого места появилась та или иная строчка.
В Java нет препроцессора, поэтому нет возможности использовать макросы __FILE__, __LINE__, как в С (эти макросы еще на этапе компиляции преобразуются в текущее имя файла и строку). Поэтому других способов дополнить вывод именем файла и номером строки кода, откуда это было напечатано, кроме как через стек-трейсы, нет.
Генерим эксепшн, у него получаем стек-трейс, берем в данном случае второй фрейм (нулевой — это метод getLocation, а первый — вызывает метод warning).
Как мы знаем, получение стек-трейса и, тем более, преобразование его в стек-трейс элементы очень дорого. А нам нужен один фрейм. Можно ли как-то проще сделать (без эксепшн)?
Помимо getStackTrace у исключения есть метод getStackTrace объекта Thread.
Будет ли так быстрее?
Нет. JVM никакой магии не делает, здесь все будет работать через тот же эксепшн с точно таким же стек-трейсом.
Но хитрый способ все-таки есть:
Я люблю всякие приватные штуки: Unsafe, SharedSecrets и т.д.
Есть аксессор, который позволяет получить StackTraceElement конкретного фрейма (без необходимости преобразовывать весь стек-трейс в Java-объекты). Это будет работать быстрее. Но есть плохая новость: в Java 9 это работать не будет. Там проделана большая работа по рефакторингу всего, что связано со стек-трейсами, и таких методов там теперь просто нет.
Конструкция, позволяющая получить какой-то один фрейм, может быть полезна в так называемых Caller-sensitive методах — методах, чей результат может зависеть от того, кто их вызывает. В прикладных программах с такими методами приходится сталкиваться нечасто, но в самой JDK подобных примеров немало:
В зависимости от того, кто вызывает Class.forName, поиск класса будет осуществляться в соответствующем класс-лоадере (того класса, который вызвал этот метод); аналогично — с получением ResourceBundle и загрузкой библиотеки System.loadLibrary. Также информация о том, кто вызывает, полезна при использовании различных методов, которые проверяют пермиссии (а имеет ли данный код право вызывать этот метод). На этот случай в «секретном» API предусмотрен метод getCallerClass, который на самом деле является JVM-интринсиком и вообще почти ничего не стоит.
Как уже много раз говорилось, приватный API — это зло, использовать которое крайне не рекомендуется (сами рискуете нарваться на проблемы, подобные тем, что ранее вызвал Unsafe). Поэтому разработчики JDK задумались над тем, что раз этим пользуются, нужна легальная альтернатива — новый API для обхода потоков. Основные требования к этому API:
-
чтобы можно было обойти только часть фреймов (если нам нужно буквально несколько верхних фреймов);
-
опция RETAIN_CLASS_REFERENCE означает, что вам нужны не имена классов, а именно инстансы;
Простейший пример, как этим пользоваться:
Берем StackWalker и вызываем метод forEach, чтобы он обошел все фреймы. В результате получим такой простой стек-трейс:
То же самое с опцией SHOW_REFLECT_FRAMES:
В этом случае добавятся методы, относящиеся к вызову через рефлекшн:
Если добавить опцию SHOW_HIDDEN_FRAMES (она, кстати, включает в себя SHOW_REFLECT_FRAMES, т.е. рефлекшн-фреймы тоже будут показаны):
В стек-трейсе появятся методы динамически-сгенерированных классов лямбд:
А теперь самый главный метод, который есть в StackWalker API — метод walk с такой хитрой непонятной сигнатурой с кучей дженериков:
Метод walk принимает функцию от стек-фрейма.
Его работу проще показать на примере.
Несмотря на то, что все это выглядит страшно, как этим пользоваться — очевидно. В функцию передается стрим, а уже над стримом можно проводить все привычные операции. К примеру, вот так выглядел бы метод getCallerFrame, который достает только второй фрейм: пропускаются первые 2, потом вызывается findFirst:
Метод walk возвращает тот результат, который возвращает эта функция стрима. Все просто.
Для данного конкретного случая (когда нужно получить просто Caller класс) есть специальный shortcut метод:
Еще один пример посложнее.
Обходим все фреймы, оставляем только те, которые относятся к пакету org.apache, и выводим первые 10 в список.
Интересный вопрос: зачем такая длинная сигнатура с кучей дженериков? Почему бы просто не сделать у StackWalker метод, который возвращает стрим?
Если дать API, который возвращает стрим, у JDK теряется контроль над тем, что дальше над этим стримом делают. Можно дальше этот стрим положить куда-то, отдать в другой поток, попробовать его использовать через 2 часа после получения (тот стек, который мы пытались обойти, давно потерян, а тред может быть давно убит). Таким образом будет невозможно обеспечить «ленивость» Stack Walker API.
Основной поинт Stack Walker API: пока вы находитесь внутри walk, у вас зафиксировано состояние стека, поэтому все операции на этом стеке можно делать lazy.
На десерт еще немного интересного.
Как всегда, разработчики JDK прячут от нас кучу сокровищ. И помимо обычных стек-фреймов они для каких-то своих нужд сделали живые стек-фреймы, которые отличаются от обычных тем, что имеют дополнительные методы, позволяющие не только получить информацию о методе и классе, но еще и о локальных переменных, захваченных мониторах и значениях экспрешн-стека данного стек-фрейма.
Защита здесь не ахти какая: класс просто сделали непубличным. Но кто же нам мешает взять рефлекшн и попробовать его? (Примечание: в актуальных сборках JDK 9 доступ к непубличному API через рефлекшн запрещён. Чтобы его разрешить, необходимо добавить опцию JVM --add-opens=java.base/java.lang=ALL-UNNAMED )
Пробуем на таком примере. Есть программа, которая рекурсивным методом ищет выход из лабиринта. У нас есть квадратное поле size x size. Есть метод visit с текущими координатами. Мы пытаемся из текущей клетки пойти влево / вправо / вверх / вниз (если они не заняты). Если дошли из правой-нижней клетки в левую-верхнюю, считаем, что нашли выход и распечатываем стек.
Если я делаю обычный dumpStack, который был еще в Java 8, получаем обычный стек-трейс, из которого ничего не понятно. Очевидно — рекурсивный метод сам себя вызывает, но интересно, на каком шаге (и с какими значениями координат) вызывается каждый метод.
Заменим стандартный dumpStack на наш StackTrace.dump, который через рефлекшн использует live стек-фреймы:
В первую очередь надо получить соответствующий StackWalker, вызвав метод getStackWalker. Все фреймы, которые будут передаваться в getStackWalker, на самом деле будут экземплярами лайв стек-фрейма, у которого есть дополнительные методы, в частности, getLocals для получения локальных переменных.
Запускаем. Получаем то же самое, но у нас отображается весь путь из лабиринта в виде значений локальных переменных:
На этом мы заканчиваем первую часть поста. Вторая часть здесь.
Лично встретиться с Андреем в Москве можно будет уже совсем скоро — 7-8 апреля на JPoint 2017. В этот раз он выступит с докладом «JVM-профайлер с чувством такта», в котором расскажет, как можно получить честные замеры производительности приложения, комбинируя несколько подходов к профилированию. Доклад будет «всего» часовой, зато в дискуссионной зоне никто не будет ограничивать вас от вопросов и горячих споров!
Кроме этого, на JPoint есть целая куча крутых докладов практически обо всем из мира Java — обзор планируемых докладов мы давали в другом посте, а просто программу конференции вы найдете на сайте мероприятия.
Статья является шуточной, но с долей правды (программирование, же). Данная статья также содержит код, который может смертельно навредить вашему зрению. Читайте на ваш риск.
Вступление
Здравствуйте. Думаю многие сталкивались с неинформативностью большинства критических ошибок, вылетающих в программе. Давайте перечислим, какие ситуации могут приводить к аварийному завершению работы программы:
Исключение
Исключения — это очень мощная система обработки исключительных ситуаций, возникающих в программе. Но если исключение не было обработано — то оно роняет программу через 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 крайне затруднительно и приходится прибегать к костылям. Но в любом случае, спасибо за прочтение данной статьи.
В языке программирования Java у программиста есть очень много способов получить информацию о том, что сейчас происходит в программе. И это не просто слова.
Например, программы на языке C++ после компиляции превращаются в один большой файл машинного кода и все, что во время выполнения доступно программисту, — это адрес куска памяти, который содержит машинный код, который сейчас выполняется. Не густо, скажем так.
В Java же, даже после компиляции, классы остаются классами, методы и переменные никуда не деваются, и у программиста есть много способов получить данные о том, что сейчас происходит в программе.
Стек-трейс
Например, в любой момент работы программы можно узнать класс и имя метода, который сейчас выполняется. И даже не одного метода, а получить информацию о всей цепочке вызовов методов от текущего метода до метода main() .
Список, состоящий из текущего метода, метода, который его вызвал, его вызвавшего метода и т.д., называется stack trace . Получить его можно с помощью команды:
Можно записать ее и в две строки:
Статический метод currentThread() класса Thread возвращает ссылку на объект типа Thread , который содержит информацию о текущей нити (о текущем потоке выполнения). Подробнее о нитях вы узнаете в 7 и 8 уровнях квеста Java Core .
У этого объекта Thread есть метод getStackTrace() , который возвращает массив элементов StackTraceElement , каждый из которых содержит информацию об одном методе. Все элементы вместе и образуют stack trace .
Код |
---|
Вывод на экран |
Как мы видим по выводу на экран, в приведенном примере метод getStackTrace() вернул массив из трех элементов:
- Метод getStackTrace () класса Thread
- Метод test () класса Main
- Метод main () класса Main
Из этого стек-трейса можно сделать вывод, что:
- Метод Thread.getStackTrace() был вызван методом Main.test() в строке 11 файла Main.java
- Метод Main.test() был вызван методом Main.main() в строке 5 файла Main.java
- Метод Main.main() никто не вызывал — это первый метод в цепочке вызовов.
Кстати, на экране отобразилась только часть всей имеющийся информации. Все остальное можно получить прямо из объекта StackTraceElement
При запуске программа заканчивается исключением. Но из текста исключения в консоли неясно, где оно возникло. В этой задаче тебе нужно: - обернуть в try-catch код методов addIce, addVodka, addJuice и addOrange; - в блоках catch вызвать метод printBugMethodName и передать в него stack trace. Если сд
2. StackTraceElement
Класс StackTraceElement , как следует из его названия, создан для того, чтобы хранить информацию по одному элементу stack trace — т.е. по одному методу из StackTrace .
У объектов этого класса есть такие методы:
Метод | Описание |
---|---|
Возвращает имя класса | |
Возвращает имя метода | |
Возвращает имя файла (в одном файле может быть много классов) | |
Возвращает номер строки в файле, в которой был вызов метода | |
Возвращает имя модуля (может быть null ) | |
Возвращает версию модуля (может быть null ) |
С их помощью можно получить более полную информацию о текущем стеке вызовов:
имя класса
имя метода
имя файла
номер строки
имя модуля
версия модуля
Метод printStackTrace принимает в качестве параметра массив stackTrace. Нужно вывести информацию о каждом элементе массива в формате: "Метод <имя метода>вызван из строки класса <имя класса>в файле <имя файла>." Информацию о каждом элементе выводи с новой строки.имя>
Читайте также: