Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Kernigan_B__Payk_R_Praktika_programmirovania.pdf
Скачиваний:
76
Добавлен:
18.03.2016
Размер:
2.53 Mб
Скачать

Отладка

Отладчики

Хорошие подсказки, простые ошибки

Трудные ошибки, нет зацепок

Последняя надежда

Невоспроизводимые ошибки

Средства отладки

Чужие ошибки

Заключение

Дополнительная литература

bug ("жучок", "баг"). b. Дефект или/неполадка в машине, плане и т. п. Происх. — США.

"Пэл Мэл Газет", 1889, 11 марта, 1/1. Мистер Эдисон, как я слышал, провел две бессонных ночи, отыскивая "жучка" в своем фонографе, — это выражение означает решение сложной проблемы, его использование подразумевает, что где-то внутри спряталось какое-то воображаемое насекомое, которое и вызывает все проблемы.

Oxford English Dictionary, 2"d Edition

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

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

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

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

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

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

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

Как бы нам ни хотелось обратного, но основное время при программировании тратится на тестирование и отладку. В этой главе мы обсудим, как сократить время, которое вы тратите на отладку, и как использовать это время наиболее продуктивно; к вопросам тестирования мы вернемся в главе 6.

Отладчики

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

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

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

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

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

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

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

Хорошие подсказки, простые ошибки

Ой! Что-то случилось. Моя программа "свалилась", напечатала какой-то мусор или, кажется, "зависла". Что мне делать?

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

К счастью, в большинстве своем ошибки просты, и их можно обнаружить с помощью простых приемов. Изучите улики — неверные результаты работы и попытайтесь догадаться, как такие результаты могли возникнуть. Посмотрите на отладочную выдачу перед аварийным завершением; если возможно, получите у отладчика стек вызовов. Теперь вы уже кое-что знаете о том, что именно произошло и где.

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

Процесс отладки включает в себя обратную трассировку (backward reasoning) — прослеживание событий в обратном порядке, как в детективе. Случилось что-то невозможное, и единственное, что известно точно, — невозможное случилось. Для того чтобы раскрыть причины, нужно мысленно проходить обратный путь от результата к возможной причине. Когда у нас имеется полное объяснение, мы знаем, что именно исправлять и, по ходу дела, скорее всего, обнаружим несколько других вещей, которых мы не ожидали.

Ищите знакомые ситуации. Спросите себя, известна ли уже вам эта ситуация. "Я уже видел это" — с этой фразы часто начинается понимание, а иногда даже и возникает ответ. Обычные ошибки имеют четко различимые признаки. Например, начинающие программисты на С часто пишут

? int n

вместо

int n; scant ("$ &п);

При такой попытке ввода значения обычно возникает ошибка обращения за пределы доступной памяти. Преподаватели языка С немедленно узнают этот симптом.

Несовпадающие типы и преобразования при вызове printf и scant рождают бесконечный поток тривиальных ошибок:

?int n = 1;

?double d = PI;

?pnntf("%d %f\n", d, n);

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

1074340347 268156158598852001534108794260233396350\ 1936585971793218047714963795307788611480564140\ 0796821289594743537151163524101175474084764156\ 422771408323839623430144.000000

Другой обычной ошибкой является использование %f вместо %lf, когда значение типа double читается с помощью scanf. Некоторые компиляторы ловят такие ошибки, проверяя, соответствуют ли типы аргументов scanf и printf параметрам форматной строки; если вывод всех предупреждений компилятора разрешен, то относительно приведенного выше обращения к printf компилятор GNU gcc сообщит

х.с:9: warning: int format, double arg (arg 2)

x.c:9: warning: double format, different type arg (arg 3)

Неинициализированные локальные переменные — еще один источник четко отличимых ошибок. Результатом часто являются слишком большие значения, возникшие из-за мусора, оставшегося в этом месте памяти от другой переменной. Некоторые компиляторы предупредят вас, если вы включите это предупреждение, но часть случаев они отследить все же не могут. Память, выделенная функциями типа malloc, realloc и new, скорее всего, также содержит мусор; обязательно инициализируйте ее.

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

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

?for (i = 1; i < argc; i++) {

?if (argv[i][0] != '-') /* аргументы кончились */

?break;

?switch (argv[i][1]) {

?case 'о1: /* имя выходного файла */

?outname = argv[i];

?break;

?case T :

?from = atoi(argv[i]);

?break;

?case 't' :

?to = atoi(argv[i]);

?break;

...

Довольно скоро nocjje опробования программы наш коллега сообщил, что имя выходного файла всегда начиналось с -о. Это было обидно, но, как оказалось, легко исправимо: код следовало читать так:

outname = &argv[i][2];

Программа была исправлена и отослана обратно, а затем пришла опять с сообщением, что программа не обрабатывала должным образом аргументы типа - f 123: преобразованное числовое значение всегда содержало ноль. Это та же самая ошибка: следующая часть оператора выбора должна была звучать так:

from = atoi(&argv[i][2]);

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

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

Не откладывайте отладку на потом. Чрезмерная торопливость может повредить и в других ситуациях. Не игнорируйте проявившуюся ошибку: отследите ее прямо сейчас, потому что потом она может и не возникнуть. Пример — знаменитая история, случившаяся при запуске космической станции "Mars Pathfinder". После безупречного "приземления" в июле 1997 года компьютеры станции имели обыкновение перезагружаться в среднем один раз в день, и это поставило инженеров в тупик. Когда они отследили ошибку, то поняли, что уже встречались с ней. Во время предпусковых проверок такие перезагрузки случались, но были проигнорированы, потому что инженеры работали над другими вопросами. Теперь они оказались вынуждены решать проблему, когда машина находится на расстоянии десятков миллионов километров, и исправить ошибку стало значительно труднее.

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

Вот типичный пример, основанный на обсуждении сортировки из главы 2. Для того чтобы отсортировать массив целых, нужно вызвать qsort с функцией сравнения целых чисел icmp:

nt arr[N];

qsort(arr, N, sizeof (arr[0]), icmp);i

Предположим, что мы по недосмотру передаем вместо icmp функцию сравнения строк scmp:

?int arr[N];

? qsort(arr, sizeof(arr[0]), scmp);

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

0 strcmp(0x1a2, Ox1c2) ["strcmp.s":31]

1 scmp(p1 = 0x10001048, p2 = Ox1000105c) ["badqs.c": 13]