Принципы отладки/основные темы в C/С++

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

Первоначально я решил, что просто прочитал бы документацию gdb и методы отладки glean из ее функциональности; однако, кроме прыжка в него, чтобы получить номер строки segfault и, возможно, запустить bt, через несколько месяцев я все еще прибегаю к массе printf в качестве моей стратегии по умолчанию. Я чувствую, что это потому, что у меня нет четко определенных стратегий, которые я мог бы использовать с помощью более сложных средств.

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

Ответ 1

У вас есть несколько прямых стратегий, которые вы должны учитывать:

  • Mass printfs платит за протоколирование. Здесь у вас много вариантов, но широковещательная регистрация не является особенно плохой стратегией, она на самом деле жизненно важна для любой формы отладки на стороне клиента.
  • Широко использовать утверждения (и никогда не отключать их, даже в коде "release" ). Всегда записывайте проверки всех возможных ошибок и выходите как можно скорее (используйте исключения на С++ - всегда бросайте, никогда не поймайте).
  • В emacs полезно использовать master gdb. Изучение того, как настроить программу, как настроить контрольные точки и как проверять локальные переменные, обычно более чем достаточно.
  • Тестирование модулей - это то, что нужно учитывать. Тем более, что небольшие тесты легче отлаживать, потому что они не окружены шумом полнофункциональной программы. Напишите тесты перед кодом или, лучше, попросите кого-нибудь написать тесты.

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

  • Обучение, как выполняется программа (например, о кадрах стека и небольшом вступлении к сборке), может оказаться полезным в определенных ситуациях, когда ошибка искажает память. В более общем плане, никогда не прекращайте изучать материал о своей среде.
  • В С++ используйте хорошие практики: RAII, стандартная библиотека, как можно скорее и т.д. Это имеет сильную тенденцию к сокращению усилий по отладке, особенно. поскольку отладчики могут довольно печально печатать материал из стандартной библиотеки. Кроме того, кодирование просто по возможности положительно влияет на время отладки.
  • Использовать (распределенный) контроль версий. Всегда. Вы увидите преимущества, когда вы привыкнете к нему (например, в сочетании с модульными тестами, у вас есть всемогущий git bisect, доступный для вас).

Ответ 2

Если вы хотите стать более продвинутым отладчиком C/С++, изучите некоторые сборки (вам не нужно быть экспертом, просто некоторые базовые знания хороши), узнайте о машинных регистрах и узнайте о своей платформе ABI ( бинарный интерфейс приложения, а именно, как работают аргументы функции и работа стека), чтобы вам не приходилось полагаться на printf всюду, чтобы узнать, что делает ваша программа. Также было бы неплохо научиться изучать память и знать, что вы ищете по адресам памяти, которые, когда вы становитесь достойными на сборке и понимаете, как машинные инструкции взаимодействуют с набором регистров, вы быстро узнаете, где чтобы найти адрес указателя или ячейку памяти для установки в качестве точки наблюдения или поиска в виде блока и посмотреть, что происходит с вашими структурами данных в данной ячейке памяти.

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

Кстати, не используйте printf... он буферизуется на stdout, что означает, что к моменту появления ошибки на выходе ваша ошибка, возможно, уже была распространена на что-то еще, например странная ошибка повреждения памяти и т.д. Даже если вы сбросите буфер для stdout, поместив символ конца строки и т.д., вы все равно закончите сообщения об ошибках мультиплексирования с нормальным выходом программы. Вместо printf используйте fprintf и выведите на stderr. Это не будет буферизировано, оно немедленно распечатает на выходе, и если вы хотите сохранить выход вашей программы, сообщения об ошибках не будут мультиплексированы с выходом программы.

Ответ 3

Я хочу еще немного изучить технику использования утверждений.

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

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

Когда я отлаживаю, я обычно обнаруживаю, что ошибки трудно найти, потому что я пропустил некоторые утверждения, позволяя краху уйти подальше от источника проблемы. Я использую процесс отладки, чтобы помочь исправить это. Я начинаю с того, где на самом деле произошел сбой, и подумайте "на этом уровне абстракции, что не так?". Если он разбился в середине функции, я мог бы понять, что параметры, переданные функции, были неправильными, поэтому я добавляю дополнительные утверждения рядом с началом функции, чтобы их уловить. Когда я запускаю его в следующий раз, и он падает, авария происходит немного раньше. Если сбой теперь происходит в верхней части функции, я поднимаюсь на один уровень стека и спрашиваю: "Почему вызывающий абонент не получил правильное значение?". Тогда я мог бы понять, что некоторые промежуточные вычисления были неправильными, поэтому я добавляю утверждение, чтобы поймать его раньше. Промежуточный расчет, возможно, был вызван другой функцией, возвращающей неправильное значение, и в этом случае я добавлю послесловие, чтобы поймать это ранее. Возможно, это связано с тем, что текущая функция не прошла правильные параметры, поэтому я добавляю к этой функции предварительное условие.

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

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

Ответ 4

Ничего плохого при использовании printf. Infact, он имеет много преимуществ:

  • Оператор printf, который был прокомментирован, является четким индикатором прошлых попыток отладки, который указывает на то, что в этой области кода может произойти что-то подозрительное.

    Операторы
  • printf понятны, если они многословны и хорошо обработаны. Даже новички понимают их смысл и способы их использования.

    Операторы
  • printf на самом деле заставляют вас думать о проблеме и что ее вызывает. Вы должны попытаться следовать логическому потоку управления и потоку данных, чтобы понять проблему. Затем это заставляет вас понять всю систему.

Infact, я думаю, что люди, которым обучают полагаться на инструменты для отладки своего кода, склонны сначала проконсультироваться со своими инструментами, прежде чем на самом деле даже попытаться выяснить причину плохого поведения в своем коде! Конечно, во многих случаях отладчиками предпочтительнее, но ясно, что человек, который думает первым, а не слепо поворачивается к отладчику, - это человек, который по своей сути лучше понимает проблему, лучше понимает ошибочное состояние программа и, в конечном счете, лучшее понимание причины проблемы.

Позвольте мне процитировать Rob Pike здесь:

"Когда что-то пошло не так, я рефлексивно начинаю вникать в проблему, изучать следы стека, вставлять в печатные заявления, вызывать отладчик и т.д. Но Кен просто встал и подумал, проигнорировав меня и код, который мы только что написали. Через некоторое время я заметил образец: Кен часто понимал проблему до того, как я это сделал, и внезапно объявил:" Я знаю, что случилось ". Он обычно был прав. Я понял, что Кен строит умственную модель кода, и когда что-то сломалось, это была ошибка в модели. Подумав о том, как эта проблема может произойти, он будет интуитивно, если модель была неправильной или где наш код не должен удовлетворять модели.

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

Ответ 5

Возможно, вам захочется прочитать книгу о "Test Driven Development" (TDD), например. Кент Бек.

Ответ 6

Узнайте, как использовать Valgrind, по крайней мере, инструмент по умолчанию Memcheck. Это сэкономит вам много времени при отладке различных проблем управления памятью, таких как:

  • Переполнение и блокирование блоков кучи
  • Использование undefined значений
  • Использование уже освобожденных объектов