Поиск по сайту:

Отладка с помощью GDB: копаем глубже


Мощный GDB-отладчик GNU возвращается на передний план. Мы погружаемся в стеки, обратные трассировки, переменные, дампы ядра, кадры и отладку глубже, чем когда-либо прежде. Присоединяйтесь к нам для совершенно нового, более продвинутого введения в GDB.

Что такое ГБД?

Если вы новичок в отладке в целом или в GDB — отладчике GNU — в частности, вы можете сначала прочитать нашу статью «Отладка с помощью GDB: начало работы», а затем вернуться к этой. Эта статья будет продолжать основываться на информации, представленной там.

Установка ГБД

Чтобы установить GDB в свой дистрибутив Linux на основе Debian/Apt (например, Ubuntu и Mint), выполните в терминале следующую команду:

sudo apt установить gdb

Чтобы установить GDB в свой дистрибутив Linux на основе RedHat/Yum (например, RHEL, Centos и Fedora), выполните в терминале следующую команду:

sudo yum установить gdb

Стеки, обратные трассировки и кадры!

Звучит как яблоки, пирог и блины! (И в какой-то степени это так.) Так же, как яблоки и блины кормят нас, стеки, обратные трассировки и фреймы являются хлебом и маслом всех разработчиков, отлаживающих в GDB, и информация, представленная в них, обильно питает разработчика. жаждущий обнаружить свою ошибку в исходном коде.

Команда bt GDB сгенерирует обратную трассировку всех функций, которые были вызваны, одну за другой, и представит нам перечисленные фреймы (функции), одну за другой. . Стек очень похож на трассировку в том смысле, что стек — это обзор или список функций, которые привели к сбою, ситуации или проблеме, тогда как трассировка — это команда, которую мы вводим для получения стека.

При этом часто термины используются взаимозаменяемо, и можно было бы спросить: «Можете ли вы достать мне стопку?» или «Давайте посмотрим обратную трассу», что несколько меняет значение обоих слов в каждом предложении соответственно.

Напомним, что в предыдущей статье о GDB frame — это, по сути, одна функция, указанная в трассировке всех вызовов вложенных функций, например, функция main(). начиная с первого (перечисленного в конце обратной трассировки), а затем main() вызывает math_function(), который, в свою очередь, вызывает do_the_maths() и т. д.

Если это звучит немного сложно, сначала ознакомьтесь с разделом Отладка с помощью GDB: начало работы.

Для однопоточных программ GDB, как всегда (если не всегда), правильно обнаружит аварийный (и единственный) поток, когда мы начнем наше приключение по отладке. Это упрощает немедленное выполнение команды bt, когда мы вводим gdb и оказываемся в подсказке (gdb), поскольку GDB немедленно покажет нам след, относящийся к сбою, который мы наблюдали.

Однопоточный или многопоточный?

При отладке дампов памяти очень важно наблюдать (и знать), является ли отлаживаемая программа (или, точнее, была) однопоточной или многопоточной?

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

Однако, как только у нас будет несколько потоков, одна команда bt (backtrace) создаст обратную трассировку только для потока, который в данный момент выбран внутри GDB.

GDB автоматически выберет аварийный сбой, и даже для многопоточных программ это в 99%+ случаев выполняется правильно. Есть только случайные случаи, когда GDB ошибочно принимает аварийный поток за другой. Например, это может произойти, если программа аварийно завершила работу в двух потоках одновременно. За последние 10 лет я наблюдал это всего несколько раз при обработке тысяч дампов ядра.

Чтобы продемонстрировать разницу между примером, использованным в нашей последней статье, и настоящим многопоточным приложением, я собрал сервер MySQL 8.0.25 в режиме отладки (другими словами, с добавленными символами/инструментами отладки), используя скрипт сборки в GitHub MariaDB-a. repo и немного обработал его SQL-данные pquery framework, что достаточно скоро привело к сбою сервера отладки MySQL.

Как вы, возможно, помните из нашей предыдущей статьи, дамп ядра — это файл, созданный операционной системой или, в некоторых случаях, самим приложением (если оно имеет встроенные средства обработки сбоев/дампа ядра), который затем может быть проанализированы с помощью GDB. Файл ядра обычно записывается как файл с ограниченными правами (для защиты конфиденциальной информации, содержащейся в памяти), и вам, вероятно, потребуется использовать свою учетную запись суперпользователя (т. е. root) для доступа к нему.

Давайте погрузимся прямо в дамп ядра, созданный с помощью gdb bin/mysqld $ (ls data/*core*):

И через несколько секунд GDB завершает загрузку и выводит нас на приглашение GDB:

Различные сообщения New LWP (которых в полном выводе было еще больше) дают хороший намек на то, что эта программа была многопоточной. Термин LWP означает Легкий процесс. Вы можете думать об этом как об эквиваленте одного потока, каждый из которых составляет список всех потоков, обнаруженных GDB при анализе ядра. Обратите внимание, что GDB должен сделать это заблаговременно, чтобы он мог найти аварийный поток, как описано ранее.

Кроме того, как мы можем прочитать в последней строке первого изображения запуска GDB выше, GDB инициировал действие Чтение символов из bin/mysqld. Без символов отладки, встроенных/скомпилированных в двоичный файл, мы бы увидели некоторые или большинство кадров, помеченных именем функции ??. Кроме того, для этих имен функций не будут представлены значения переменных.

Эту проблему (неразрешимые кадры, наблюдаемые при отладке определенных оптимизированных/удаленных двоичных файлов, у которых были удалены или удалены символы отладки) решить нелегко. Например, если бы вы увидели это на двоичном файле сервера базы данных производственного уровня (у которого символы отладки удалены/убраны для оптимизации времени выполнения и т. д.), вам пришлось бы следовать более сложным процедурам, таким как, например, Как Создайте полную трассировку стека для mysqld.

Обратные следы!

Поскольку мы скомпилировали сервер MySQL с включенными символами отладки, трассировка правильно отобразит все имена функций в нашем случае. Мы запускаем команду bt в командной строке (gdb), и наш вывод обратной трассировки выглядит следующим образом:

Итак, как мы можем увидеть обратную трассировку для всех потоков или другого потока? Этого можно добиться с помощью команд thread apply all bt или thread 2; bt соответственно. Мы можем поменять местами 2 в последней команде, чтобы получить доступ к другому потоку и т. д. Хотя вывод thread apply all bt немного подробен для вставки сюда, вот вывод, когда переключиться на другой поток и получить обратную трассировку для этого потока:

Внимательное прочтение любого журнала компьютерных ошибок или трассировки, как всегда, позволит выявить дополнительные детали, которые легко упустить при беглом взгляде на информацию. Это настоящее умение. Один из моих предыдущих ИТ-менеджеров сообщил мне о большой необходимости сделать это, и настоящим я передаю ту же информацию всем активным читателям этой статьи. Чтобы подкрепить это утверждение некоторыми доказательствами, внимательно посмотрите на созданную обратную трассировку, и вы заметите термины listen_for_connection_event, poll, Mysqld_socket_listener, и connection_event_loop для Mysqld_socket_listener. Это совершенно ясно: этот поток ожидает ввода.

Это просто бездействующий поток, который, вероятно, ждал подключения клиента MySQL или ввода новой команды или чего-то подобного. Другими словами, продолжение отладки этого потока будет равно нулю.

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

Интересно, что здесь у нас есть thread 3, который тоже находится в каком-то цикле опроса и выглядит как (LinuxAIOHandler::poll), хотя в данном случае он находится в ОС /Disk (что обозначается терминами Linux, AIO и Handler), и при ближайшем рассмотрении видно, что он, похоже, ждет, для завершения AIO: fil_aio_wait.

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

Вот совет: вы можете использовать команду set log on в GDB, если вы хотите сохранить всю информацию на диск, чтобы вы могли легко найти вывод позже, и вы можете использовать set выйдите из системы, чтобы завершить выходную трассировку. Информация сохраняется в gdb.txt по умолчанию.

Прыжки в кадры

Точно так же, как мы видели, можно переключаться между потоками и даже получать обратную трассировку для всех журналов одновременно, и точно так же можно переходить к отдельным кадрам! Мы даже можем — при условии, что исходный код доступен на диске и хранится в исходном месте на диске (т. е. в том же каталоге исходного кода, который использовался при сборке продукта) — просмотреть исходный код для конкретного кадра, который мы повторно в.

Здесь нужно соблюдать некоторую осторожность. Довольно легко не сопоставить двоичные файлы, код и дампы ядра. Например, попытка проанализировать дамп ядра, созданный с помощью версии v1.0 данной программы, вероятно, не будет совместима с бинарным файлом версии v1.01, скомпилированным несколько позже с кодом v1.01. Кроме того, нельзя [всегда] использовать исходный код версии 1.01 для отладки дампа ядра, написанного с помощью версии программы версии 1.0, даже если доступен двоичный файл версии 1.0.

Слово всегда было помещено как необязательное, так как иногда — если код в этом разделе кода и отлаживаемой программы не изменился со времени последней версии — может быть возможно использовать более старый исходный код. .

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

Тем не менее, если вы анализируете множество ядер, которые прислали клиенты, часто с ограниченной информацией, иногда можно обойтись использованием немного другой версии исходного кода и, возможно, даже немного другого двоичного кода (что менее вероятно), хотя , всегда помните, что информация, представленная GDB, скорее всего, будет недействительной частично или, что более вероятно, полностью. GDB также предупредит вас при запуске, если сможет обнаружить несоответствие между ядром и двоичным файлом.

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

Здесь есть еще одно небольшое исключение, и это разрушение стека. В таком случае вы либо будете наблюдать сообщения об ошибках в GDB, см. имена кадров ?? — аналогично ситуации, описанной выше (но на этот раз из-за нечитаемости дампа ядра в сочетании с двоичным файлом) — иначе стек будет выглядеть очень странно и неправильно. Большую часть времени это будет совершенно ясно. Иногда действительно серьезная ошибка может привести к разрушению стека.

Давайте теперь перейдем во фрейм и посмотрим, как выглядят некоторые из наших переменных и кода:

Переменные

t 1
bt
f 7
frame 8
p *thd
p thd

Здесь мы ввели различные команды, чтобы перейти к нужному потоку и запустить обратную трассировку (t 1 привела нас к первому потоку, потоку сбоя в нашем примере, за которым последовала команда обратной трассировки bt), а затем перешел к кадру 7, а затем к кадру 8 с помощью команд f7 и frame 8 соответственно. Вы можете видеть, как, подобно команде thread, можно сократить команду frame до ее первой буквы, f.

Наконец, мы попытались получить доступ к переменной thd, хотя она была оптимизирована из дампа трассировки/ядра для этого конкретного кадра. Однако информация доступна, если мы просто перейти к правому фрейму, который имеет доступную переменную и не был оптимизирован (может потребоваться немного проб и ошибок):

На последних двух снимках экрана выше я показал два разных способа ввода команды print (опять же, сокращенно аналогично просто p), первый с начальным * для имени переменной, второй без).

Интересно здесь то, что второй используется чаще, но обычно предоставляет только адрес памяти для рассматриваемой переменной, что не очень удобно. Версия команды * (p *thd) вместо этого разрешает переменную до ее полного содержимого. Кроме того, GDB знает тип переменной, поэтому нет необходимости приводить ее к типу (приводить значение к другому типу переменной).

Подведение итогов

В этом более подробном руководстве по GDB мы рассмотрели стеки, трассировки, переменные, дампы ядра, фреймы и отладку. Мы изучили несколько примеров GBD и дали несколько важных советов для заядлых читателей о том, как правильно и успешно проводить отладку. Если вам понравилось читать эту статью, взгляните на нашу статью «Как работают сигналы Linux: SIGINT, SIGTERM и SIGKILL».