?

Log in

No account? Create an account
 
 
11 Ноябрь 2008 @ 13:29
Рецепты отладки. 3 типа нестабильностей.  
Разных нудных и занимательных (нужное подчеркнуть) историй можно рассказывать ещё много, но надо бы заняться уже каталогизацией возникающих в приложениях проблем. Точно также, как врач ставит пациенту диагноз, основываясь на каких то симптомах, программист может найти ошибку в коде, основываясь на том, как она проявляется. Поэтому я попробую обобщить разную информацию об ошибках и о том, как они проявляются.
Но для начала - ещё одно лирическое отступление на несколько страниц.

Можно лечить ошибки, а можно их предупреждать. Лучше заранее представлять себе, какие потенциальные ошибки могут возникнуть в коде и страховаться от них. Приведённая ниже классификация поможет лучше ориентироваться в причинах возникновения проблем в коде и в способах их диагностики. Эта классификация опирается не на сами ошибки (я потом тоже буду рассматривать разные варианты ошибок и их проявлений), а на ситуации, которые предшествуют появлению ошибки. Я называю эти ситуации “нестабильностями”.

Все возникающие в программе нестабильности можно разделить на 3 главных типа. Это:
  1. Нестабильности по входным данным.
  2. Нестабильности по внешним вызовам.
  3. Внутренние нестабильности модуля.
Рассмотрим подробнее.

1. Нестабильность по входным данным. Проявляются в том случае, если аргументы, переданные в функцию, не соответствуют ожидаемым. Простейший пример такой нестабильности - strcpy(NULL, “Умри, сцуко!”). Более сложные примеры могут содержать нестабильности, которые проявляются не сразу - strcpy(malloc, “Умри, сцуко!”). Или например, нестабильность по входным данным, связанная с передачей адреса локальной переменной sz на стеке в функцию SQLBindCol() (этот пример рассмотрен в http://ddima.livejournal.com/43027.html).

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

Очень частой ситуацией появления нестабильности по входным данным является изменение поведения на граничных или на запрёщенных входных данных. Допустим, программист реализует функцию, которая в ответ на 2 и 2 дает результат 4. При этом функция реализована через умножение аргументов. Очевидно, что на 2 и 2 поведение функции является хорошо отлаженным и работоспособным. Если же передать в функцию 3 и 1, результатом станет 3. Нули являются запрещёнными аргументами, хотя функция как-то работает и на них.

Теперь мы оптимизируем алгоритм и делаем реализацию через сложение. Для “стандартных” 2 и 2 ответ не меняется. А вот для 3 и 1 ситуация кардинально изменилась - теперь возвращается результат 4. Если где-то в программе по каким-то причинам вызывалась функция с пограничными результатами, то поведение программы в этом месте начнет сильно отличаться. Ну и если передавались нули, то программа вообще начнёт, например, падать.

Много отладок программ, которые заканчиваются моей любимой фразой “я не понимаю, как оно вообще тут работало” как раз и связано с тем, что программисты забывают о пограничных или запрещенных значениях аргументов и поведение функций начинает отличаться именно в этих ситуациях (а не в 2*2, которые являются “стандартными”).

2. Нестабильность по внешним вызовам.
Проявляется в том случае, если программист осознанно (или неосознанно, например, через действия пользователя) нарушает порядок вызова функций в ту или иную библиотеку (или порядок вызова методов в объект). Простейший пример возникновения такой нестабильности был рассмотрен в http://ddima.livejournal.com/43589.html, где вызывались функции GDI+ после GdiplusShutdown(). В более сложных ситуациях модуль/объект может находиться в дюжине различных состояний, между которыми он переключается при помощи вызова разных функций.

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

Очень важным в понимании нестабильностей второго рода является тот момент, что надо отдельно выделить “нулевое состояние” объекта. Это то состояние, когда объект только конструируется или уничтожается. Следует помнить, что уничтожение объекта может происходить из любого состояния и оно обязано происходить корректно. Если у вас включен SEH, то ситуация с уничтожением объекта только усложнаяется.
Во всех случаях необходимо следить на состояниями. Если вы вызываете функцию, которая переводит объект из одного состояния в другое, и в процессе перехода происходит ошибка, необходимо корректно “откатить” все изменения, чтобы объект остался в исходном состоянии, а не завис в промежуточном виде между двумя стабильными стейтами.

Для внешних пользователей объект обязан предоставлять информационные функции, с помощью которых можно будет определить состояние системы. Требования к этим функциям я распишу позже.

3. Внутренняя нестабильность модуля.
Это, как правило, либо собственные ошибки алгоритма, либо отсутствие правильной обработки возвращаемых результатов из вызываемых функций. К внутренней нестабильности относится, например, эта история, где ошибочный возврат из random() 4 месяца приводил к порче оперативной памяти.

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

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

Более подробно по различным состояниям систем и нестабильностям распишу ещё в отдельных постах. Каменты всячески приветствуются.

Приятного вам кодирования и поменьше отладки!
Crossposted to blog.gamedeff.com
Метки: ,
 
 
 
i_love_pythoni_love_python on Ноябрь, 11, 2008 12:52 (UTC)
с большим интересом читаю ваши статьи про отладку и не только. однако... неужели гейм-программисты не знают такого термина как "конечный автомат"? и понимают только английскую кальку?
Дядя Дима: officeddima on Ноябрь, 11, 2008 13:08 (UTC)
Ну, это наверное, скорее, личные предпочтения каждого. Лично мне "стейт-машину" набирать короче, чем "конечный автомат" :).
Но я постараюсь исправиться!!! :))))
Qbitbik_top on Ноябрь, 11, 2008 13:39 (UTC)
Под конечным автоматом понимают одно из двух: конечный-автомат-в-узком-смысле и конечный-автомат-в-широком-смысле.

Конечный-автомат-в-узком-смысле — это математическая абстракция, пятёрка (Q, Σ, δ, q₀, F).

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

Сложились определённые традиции употребления терминов «конечный автомат» и «машина состояний». Под «машиной состояний» понимают конечный-автомат-в-широком-смысле, а под «конечным автоматом» — чаще конечный-автомат-в-узком-смысле.

>и понимают только английскую кальку?

В английском есть обе аббревиатуры: FSM — Finite State Machine, DFA — Deterministic Finite Automaton.
Дядя Дима: officeddima on Ноябрь, 11, 2008 14:56 (UTC)
На самом деле данная терминология - она сильно институт-специфик. Допустим, даже питерские Политех и Универ в заимствовании и интерпретации ряда терминов сильно отличаются друг от друга. Про другие ВУЗы я и не говорю уже.
На самом деле оно не сильно важное. Я про состояния модуля еще отдельный пост распишу (по просьбам трудящихся в привате), надеюсь, там вопросов специфической терминологии не возникнет.
i_love_pythoni_love_python on Ноябрь, 13, 2008 22:10 (UTC)
Спасибо за столь подробное изложение. Извините, что продолжаю занудничать, но DFA -- это всего лишь подмножество Finite State Machines. Поэтому согласиться с вашим пониманием не могу.

Мой начальный комментарий собственно был про то, что как бы нехорошо обижать русский язык, если нужный термин в нем есть. А такой термин есть: конечные автоматы, или просто -- автоматы.
Тем более, что в России есть даже несколько школ автоматного программирования. Очень полезным для меня в свое время оказался сайт www.softcraft.ru
Qbitbik_top on Ноябрь, 13, 2008 22:48 (UTC)
>но DFA -- это всего лишь подмножество Finite State Machines.

Хм, так я ж вроде об этом и говорю. Но у Дяди Димы в посте ведь речь идёт не о DFA, а о FSM.

>как бы нехорошо обижать русский язык

Я не предлагаю обижать русский язык. Я предлагаю употреблять термин «(конечный) автомат» там, где подразумевается DFA (например, в научной работе по формальным грамматикам), и термин «машина состояний» там, где подразумевается FSM (например, состояния объектов в игре). Считаю, что одного термина для этих двух понятий мало.

>Тем более, что в России есть даже несколько школ автоматного программирования.

Мне встречались адепты одной из школ. Некоторые даже не отдавали себе отчёт в том, почему перебор вариантов текущего состояния плохо делать switch'ем, и как это делать через таблицу функций/функторов. И всерьёз считали себя хранителями тайного знания, недоступному большинству программистов. Помню восторженную статью одного из таких авторов о прелестях автоматного программированися. Причём иллюстрирующий код на Дельфи был настолько ужасен, что становилось ясно — опыта промышленного программирования у автора нет никакого, но он искренне верит, что «промышленные программисты» знать не знают об автоматном программировании. Новооткрытая Америка, блаженное неведенье.

>Очень полезным для меня в свое время оказался сайт www.softcraft.ru

Да, мне попадались толковые статьи на этом сайте. Впрочем, в плане навыков программирования машин состояний на меня больше повлияла более приземлённая вещь — документация к Boost.Statechart. Вот, например, типичная UML-диаграмма маленькой машины состояний: History2.gif. Конечным автоматом в классическом понимании Ахо—Ульмана это никак не назовёшь.
toprighttopright on Ноябрь, 12, 2008 10:53 (UTC)
> но надо бы заняться уже каталогизацией возникающих в приложениях проблем.

Даешь периодическую таблицу проблем!
Дима, как и твой тезка Дмитрий Менделеев, это дао твоего имени - разработать уже таблицу. =)
Пушыстый_winnie on Ноябрь, 25, 2008 12:17 (UTC)
Нужен-то всего один гений!