Evg |
СОДЕРЖАНИЕ
Статическая отладка программ
Запись от Evg размещена 15.02.2012 в 23:26
Показов 29151
Комментарии 0
|
ВНИМАНИЕ! Вопросы по существу обсуждаемого вопроса просьба задавать здесь или создать тему на форуме и кинуть на неё ссылку в блог или мне в личку.
Объясняю почему
Причин для этого несколько. Я, как и любой другой автор, всегда могу упустить интересный момент обсуждаемой темы (что подтвердилось на практике). А потому задаваемый вопрос может закрывать пробел в статье. Ответ на конкретный вопрос, как правило, дать несложно. Сложнее его аккуратно сформулировать так, чтобы ответ являлся законченной частью статьи. Поэтому, как правило, на первых порах я ограничиваюсь конкретным ответом на конкретный вопрос, а в статью временно вставляю ссылку на пост, где был дан ответ. А когда дойдут руки, то вместо ссылки пишу нормальное пояснение. Технические возможности блога не позволяют в комментариях пользоваться широкими возможностями, доступными на форуме (то как выделение текста жирным, вставка фрагментов исходников в удобном для чтения виде и т.п.), поэтому будет удобнее, если вопрос и ответ будут опубликованы на форуме Любая статья является изложением знаний в общем случае. У многих людей мышление устроено так, что прочтя на форуме конкретный вопрос и конкретный ответ на этот вопрос, у них появится бОльшее понимание, чем после прочтения теоретических выкладок (даже если они подкреплены конкретными примерами). Ссылки на такие обсуждения я, как правило, включаю в последний раздел статьи. Начинающие, как правило, поиск ответов на свои вопросы ведут именно в форуме, а не в блогах. А потому конкретный вопрос и конкретный ответ для них будет более удобным и полезным именно на форуме. Многие люди умеют работать методом тыка, лишь бы был конкретный пример в качестве образца. А потому такое обсуждение будет им полезным даже без прочтения статьи Исторически сложилось, что раньше (когда ещё не было блога) статьи располагались на форуме и представлены были в виде двух тем. Первая тема создавалась в специально отведённой свалке и представляла собой черновик, который со временем дорабатывался до законченной статьи. После этого статья переезжала во вторую тему в тематическом разделе. А первая тема оставалась дополнительной свалкой для замечаний и мелких вопросов по теме. Ссылку на старое местоположение данной свалки я помещаю в начале статьи. Вопросы, по возможности, прошу создавать в отдельных темах, но если вопрос действительно мелкий, то можно его задать и в указанной свалке.
1. Введение FIXME написать 2. Внутренний контроль допустимых значений (ASSERT'ы) 2.1. Пример поздно проявляющейся ошибки В качестве примитивного примера возьмём задачу, в которой надо вычислить значение выражения
Если бы выражение было более сложным и данные рассчитывались в разных модулях программы, то найти ошибку было бы непросто. Если бы мы имели дело с какой-нибудь долго работающей программой, то могло бы получиться так, что от времени возникновения ошибки (порча данных) до времени её проявление (выяснение факта неправильного результата) могли бы пройти часы, дни или даже месяцы работы программы. И в таких условиях никакой отладчик или просмотр кода глазками скорее всего не помог бы. 2.2. Включаем контроль правильности входных данных Из-за чего произошла ошибка в примере из прошлого раздела? Из-за того, что в функциях init_a и init_b мы провели некоторые вычисления, полагая при этом, что на вход функции будет подано правильное значение i. Для раннего обнаружения ошибки можно было бы в эти функции включить контроль за правильностью параметра i. Например, функция init_a могла бы выглядеть так:
2.3. Общее понятие ASSERT'а Дополнительный if в примере из предыдущего раздела можно немного усовершенствовать в сторону удобства. Для этого используется технология, называемая assertion (предположение). Смысл сего дела в том, что такие проверки делаются при помощи макроса. Почему это должно быть именно макросом, я попытаюсь продемонстрировать на примере простенькой реализации с его постепенным улучшением. Т.е. последовательно пройти все этапы с целью понять, почему та или иная деталь реализации выполнена таким образом, а не другим. Начнём с самого простого. Мы видим, что все наши if'ы выглядят схожим образом: if (условие) abort. Во избежание копипэйстов общие части можно засунуть в тело макроса.
Итак, реализовав контроль в виде макроса мы предельно упростили внешний вид программы в точке контроля. При этом мы всегда можем макрос ASSERT обратить в пустышку. В этом случае весь контроль правильности данных из программы будет исключен буквально одним лёгким движением.
Естественно, что в каждой конкретной программе будет свой критерий выбора, какую из реализаций ASSERT'а цеплять. Управление выбором через нолик или единичку я привёл лишь условно. На будущее при написании реализации ASSERT'а я буду опускать реализацию-пустышку, чтобы не разводить ненужный мусор 2.4. Реализация ASSERT'а для начинающих Для дальнейших экспериментов положу перед глазами исходник нашей программы, с добавленными в неё ASSERT'ами. В данном разделе мы оставим программу неизменной, а менять будем лишь наполнение макроса ASSERT
Препроцессор имеет встроенные макросы __FILE__ и __LINE__, которые заменяются на имя файла и номер строки в месте их возникновения. Используя эти конструкции макрос ASSERT можно переписать следующим образом
Едем дальше. При реализации тела макроса при помощи оператора # можно превратить параметр макроса в строковой вид. Напишем наш макрос следующим образом:
2.5. Реализация ASSERT'а для продвинутых Реализации макроса ASSERT, к которой мы пришли в разделе 2.4, для начинающих программистов на первом этапе будет достаточно. Тем более, что описания в учебниках, которые я когда-то встречал, на данном этапе как правило заканчиваются. Начинающим данный раздел 2.5 (со всеми подразделами 2.5.*) можно пропустить без ущерба в понимании всей статьи, и вернуться к прочтению позже 2.5.1. Заключаем тело в фигурные скобки При частом использовании ASSERT'а рано или поздно вы можете столкнуться с ситуацией наподобие следующей:
Самое тупое решение - это заключить в фигурные скобки действие 5-й строки (т.е. точку подстановки ASSERT'а). Решение тупое, потому что такое решение может идти вразрез с стилевыми соглашениями, не говоря уж о том, что об этом можно банально забыть. Следующее, что приходит на ум - это затащить фигурные скобки в тело макроса ASSERT:
Выходом из такой ситуации служит хак (или чит) - заключить тело макроса в цикл do-while, у которого исполнится только одна итерация
2.5.2. Удаляем экранирующие круглые скобки На практике частым случаем является вызов функции, которая возвращает значение, но при этом само значение не используется. Статистически частым подмножеством этого является функция, которая выполняет какие-то действия, и возвращающая булевский результат, означающий "были ошибки или не было ошибок". Вот один из примеров, с которым мы столкнулись на практике и хорошо наступили на грабли:
Однако засада есть и кроется она вот в чём. Хоть и редко, но бывают случаи, когда на месте операций сравнения действительно находится присваивание и автор хочет сделать присваивание и сравнение результата с нулём. А потому компилятор должен предоставить программисту возможность написать такой код таким образом, чтобы не возникало предупреждений. По негласным соглашениям для таких случаев программист должен экранировать присваивание дополнительными круглыми скобками. Т.е. если написать "if (x = 0)", то компилятор выдаст предупреждение, а если написать "if ((x = 0))" - то нет. В нашем случае при реализации макроса так же использовалось дополнительное экранирование круглыми скобками. Без него нельзя гарантировать, что оператор "!" будет вычислен самым последним. С виду единственным надёжным выходом из ситуации является написание if'а без восклицательного знака, чтобы не приходилось делать экранирование круглыми скобками. В данном случае я опущу конструкцию do-while из предыдущего раздела.
Недавно на работе мы начали переход со сборки проекта компилятором gcc-3.4.6 на более современную версию компилятора gcc-4.x.x. В ситуации из предыдущего раздела
В рамках одного конкретного проекта на данное ограничение можно плюнуть с высокой колокольни. Но вот если делается какая-то пользовательская библиотека, то такое решение, к сожалению, не прокатит. В частности, когда мы искали ответ на вопрос, то мы натолкнулись на обсуждение реализации данного макроса в заголовочном файле assert.h, входящего в состав стандартной поставки gcc. В Си++ так же есть стандартный заголовочной файл cassert, внутренности которого должны соответствовать данному требованию. 2.5.4. Итоговая продвинутая реализация ASSERT'а Теперь можно написать итоговую продвинутую реализацию макроса ASSERT:
Если у человека возник интерес к использованию ASSERT'ов, то сразу же появляется вопрос: а где их лучше ставить. Универсального ответа на этот вопрос нет. Всё должно исходить из личного опыта. Так или иначе программист всегда закладывается на какие-то предпосылки: предполагаем, что переменная положительная, предполагаем, что переменная является индексом массива и находится в диапазоне, предполагаем, что здесь мы работаем только с значениями enum'а и т.п. По началу можно работать таким образом, что ставить ASSERT'ы только в тех местах, где возникла реальная ошибка. Со временем появится некоторое внутреннее ощущение того, в каких местах лучше всего расставлять ASSERT'ы. По тем или иным соображениям, бывают ситуации, когда приходится вводить некоторые понятия типа "приоритеты ASSERT'а". С ходу на пальцах понятную ситуацию придумать не получается, но такие случаи действительно бывают. В этом случае можно реализовать систему ASSERT'ов по приоритетам, как это написано здесь или здесь FIXME Про приоритеты добавить примеры, потому что неудобно читать, когда надо скакать по ссылкам 2.7. Тонкости работы с ASSERT'ами FIXME Написать по поводу того, что параметром ASSERT'а нельзя делать действие FIXME Написать про альтернативные тестовые макросы https://www.cyberforum.ru/faq/... ost1906409 3. Внутренний контроль недостижимых участков кода (FATAL'ы) В программировании довольно часто встречаются случаи, когда в программе имеются участки кода, в которые мы не должны попасть. Классический пример на эту тему - switch, ключом которого является значение типа enum:
Чтобы не наступать на грабли в такой ситуации, нужно явным образом включить ветку исполнения в текст программы и поставить в ней ловушку:
Конечно же в каждое такое место вставлять вызов printf'а (или что нам там нужно) неудобно, а потому по аналогии с макросом ASSERT нужно реализовать какой-нибудь макрос. Обычно его называют FATAL. Самая простая реализация данного макроса выглядит как:
Ещё один пример по данной теме. У нас имеется массив, все элементы которого различны. Нужно реализовать функцию, которая выполняет операцию, обратную к индексации. Т.е. по значению элемента нужно вернуть индекс этого элемента.
Точно так же в ветке default в программе со switch'ом (написанной в начале данного раздела) в переменную x можно было бы записать "плохое" значение, которое в случае пустой реализации FATAL'а так же поломало бы исполнение программы как можно ближе к точке возникновения ошибки. 4. Внутренний контроль целостности данных (магические числа) FIXME Написать 5. Внутренние и внешние ошибки FIXME Написать 6. Что делать при срабатывании внутреннего контроля Изначально данный раздел находился внутри раздела с ASSERT'ами, а потому тут речь идёт об ASSERT'ах. Но при чтении надо понимать, что вместо ASSERT'а может быть любой из способов внутреннего контроля. FIXME надо более аккуратно переписать Я уже писал о том, что ASSERT может работать в отладочном режиме (контроль включен) или в боевом режиме (контроль выключен). Так же я писал и о том, что ASSERT желательно оставлять в боевом режиме, если к этому нет каких-то противопоказаний (скорость работы, требования заказчика и т.п.). Это должно облегчить сопровождение программы, у которой много пользователей и имеется большая комбинация различных входных данных (т.е. когда программист собственным тестированием не в состоянии покрыть все жизненные ситуации). Поэтому включенный внутренний контроль в момент работы программы у пользователя - это ещё одна статья тестирования. И в момент срабатывания ASSERT'а программа должна вывести некоторую полезную (для программиста) информацию и просьбу к пользователю передать эту информацию разработчику с описанием ситуации, в которой воспроизвелась ошибка. В нашем примере внутри ASSERT'а мы выводили печать в консольном режиме и завершали работу программы. Но в каждой конкретной программе действия могут выполняться разные. Ошибки, накрытые ASSERT'ами, могут иметь разную критичность. Так, например, выход за границу массива в момент записи очень часто приводит к сбоям, потому что портятся чужие данные. А вот выход за границу в момент чтения очень часто НЕ оказывается фатальным, потому как скорее всего произойдёт какая-то локальная кривота (что-нибудь не так напечатается или нарисуется) без фатальных последствий для работы всей программы. Поэтому в теле ASSERT'а можно предложить пользователю сделать выбор: немедленно завершить работу программы, сохранив все важные данные, или продолжить работу на свой страх и риск. У нас на работе в ASSERT'ы встроена печать стека. Можно так же делать распечатку наиболее важных глобальных переменных, по значениям которых программист может либо локализовать ошибку, либо сразу же выяснить причину ошибки. Было бы полезно реализовать механизм, при помощи которого пользователю было бы просто и удобно отправить разработчику всю ту информацию, которую выдала программа в момент срабатывания ASSERT'а. Например, идеальным вариантом для GUI-программы была бы кнопочка "отправить отчёт", при нажатии на которую выскакивало бы окно почтового клиента, в котором уже был бы сформирован полный текст письма, а пользователю осталось бы только нажать на "Отправить". В общем случае действия по срабатыванию ASSERT'а определяются разработчиком программы, его фантазией и желанием сделать программу более удобной для пользователя даже в тех случаях, когда происходит сбой работы программы. Именно поэтому я НЕ использую стандартные реализации из стандартных заголовочных файлов assert.h или cassert, а всегда делаю свою реализацию 7. Трассировка работы программы FIXME Написать FIXME "if (DEBUG)" vs "#ifdef DEBUG" 8. Ссылки на темы, где обсуждался данный вопрос assert.h | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Размещено в Статьи по программированию
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии


