?

Log in

No account? Create an account
 
 
29 Октябрь 2009 @ 09:46
Теория ошибок. Нестабильности третьего и четвертого рода.  
Развивая тему анализа нестабильностей, которую я начал еще весной (http://ddima.livejournal.com/48573.html и http://ddima.livejournal.com/49288.html), оставалось рассмотреть нестабильности третьего рода - это собственные ошибки функции. То есть когда все наружные условия соблюдены, объект или модуль находятся в правильном состоянии, все переданные аргументы являются правильными, а результат работы функции все равно оказывается некорректным. Но как показала последующая практика, эти нестабильности полезно разделить еще на два независимых вида - это нестабильности, связанные с ошибками алгоритма, непроинициализированными переменными и т.п. (нестабльности третьего рода) и нестабильности, связанные с некорректной обработкой результатов функций (четвертого рода).

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

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

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

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

Вот свежий пример. Программист, используя внешнюю библиотеку, загружал файл описания, и затем по данному файлу проводил какие-то действия. Файл должен был в том числе содержать строчку ServerUrl, которая ведет на сервер авторизации. Двумя вызываемыми функциями были: loadFile() и register().

Баг заключался в том, что если вызвать функцуию register() без загрузки файла, то система падала в access violation по вызову strlen(ServerUrl = NULL). Какие тут есть нестабильности?

Первая нестабильность - в самой библиотеке по внешним вызовам (2-го рода). Библиотека оказалась не готова к тому, что приложение продолжит свою работу, если файл данных не смог загрузиться.
Две нестабильности - в приложении (обе - 4-го рода). Программа допустила гипотезу, что рабочий каталог - "хороший" и не отработала код возврата loadFile(), продолжив свою работу. Иногда это, кстати, оправдано. Например, если зафейлилась инициализация звука, то нет смысла прерывать программу - пусть она работает, но в mute режиме. Но если у вас в коде g_pDirectSoundDevice = NULL, то программа начнет стабильно падать - такое даже в топ-10 самых нелепых падений игр есть (http://ddima.livejournal.com/49050.html).
Последняя нестабильность - в функции strlen() - нестабильность первого рода по входным данным. Причем - если бы функция strlen() вернула бы нулевой или отрицательный ответ - то дальше функция выполнилась бы до конца и просто отдала наружу результат "Невозможно обратиться к регистрационному серверу".

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

Как лечить нестабильности? Для всех них есть хорошие инструменты, которые можно и нужно применять.

1-й род. Нестабильности по входным данным. Используются пре-кондишены + корректная обработка параметров входа и реализация правильного отклика на неверные входные данные. Хорошо подвергается юнит-тестированию, если система тестирования умеет проверять функции с ассертами. Работает визуальный анализ кода.
2-й род. Нестабильности по внешним вызовам. Используются пре-кондишены + проверка состояния объекта или библиотеки. Очень в этом помогает представление в виде графа состояния объекта - в нашем примере достаточно было бы сделать 2 состояния: "File not load" и "File load". Функция register(), вызванная из неправильного состояния, должна сразу вернуться обратно с кодом ошибки.
3-й род. Собственная нестабильность модуля. Используются пост-кондишены + разные runtime диагностики (check uninitialized vars, buffer security, и т.д.). Отлично работают юнит-тесты в "классическим" представлении - когда тестируется только рабочая функциональность. Также работает визуальный анализ кода.
4-й род. Отсутствие обработки вызываемых функций. Кондишены тут не нужны - ассертировать, что файл открылся или память распределилась никакого смысла нет. NULL ответ на malloc() или fopen() - это правильная ситуация, которая описана в документации. Нужно проверять и обрабатывать коды возврата. В то же время, если проверять абсолютно все и всегда - это можно и помереть раньше времени. Способы предотвращения нестабильностей - в первую очередь визуальный анализ кода и логгирование вызовов - чтобы можно было определить, что именно надо обрабатывать, особенно если проблемы начались с API-вызовами на машине у пользователя.

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

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

Приятной вам отладки.
Метки: ,
 
 
Местонахождение: office
 
 
 
cd_ripercd_riper on Октябрь, 29, 2009 07:40 (UTC)
> Нужно проверять и обрабатывать коды возврата. В то же время, если проверять абсолютно все и всегда - это можно и помереть раньше времени.

Исключения. Штуки, которые нельзя проигнорировать или про которые нельзя "забыть", и которые позволяют сводить обработку ошибок целой ветки кода в одну точку.

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

Дядя Димаddima on Октябрь, 29, 2009 12:02 (UTC)
Если проверять аккуратно, то что исключения, что ручные if() - почти монопенисуально.
Если проверять неаккуратно, то да, исключения будут компактнее.
Если речь идет о вызове API типа виндовского, то там будет заметно более громоздко, пока не написать нужные классы-врапперы (не забываем про то, что бросая исключение, может потребоваться закрыть какой-то хендл).
Я в коде типа "С с классами" иногда пользую секвенцию типа bool rc = true; rc = rc && someFunc(); с общей проверкой в конце.
cd_ripercd_riper on Октябрь, 29, 2009 12:21 (UTC)
> пока не написать нужные классы-врапперы

а кто ж мешает их написать?
тем более, что эти API обычно очень низкоуровневые, неудачные, дырявые и не объект ориентированные?

> с общей проверкой в конце.

тоже не очень хорошо, если вызываемые функции по логике взаимосвязаны.
если fopen не сработал, то я бы внутри fread или fwrite вообще ставил бы ассерт. ибо не фиг.
Дядя Димаddima on Октябрь, 29, 2009 12:31 (UTC)
> тем более, что эти API обычно очень низкоуровневые, неудачные, дырявые и не объект ориентированные?
Насколько я в наших с тобой дискуссиях сумел разобраться - все, что не объектно-ориентированное, является низкоуровневым, неудачным и дырявым по определению. Разве не так? :) :) :)

> если fopen не сработал, то я бы внутри fread или fwrite вообще ставил бы ассерт. ибо не фиг.
NULL PTR в fopen() - это нестабильность по входным данным, которая должна быть обработана. И ассертом тут не отделаешься - требуется корректная обработка (учитывая, что время этой обработки много меньше времени, которое тратится внутри функции).
cd_ripercd_riper on Октябрь, 29, 2009 12:39 (UTC)
> Разве не так? :)

не так.

но сишные API а-ля Win32 API, или беркли сокеты, или zlib или еще чего, это ужас-ужас-ужас и использовать это без красивой и надежной обертки строго себе во вред.
Иван Василичvivkin on Октябрь, 29, 2009 12:57 (UTC)
лол. ну и бред. все обвертки, над сокетами, ос-апи, всегда были и останутся бесполезным говном
cd_ripercd_riper on Октябрь, 29, 2009 13:15 (UTC)
место троллям -- в лесу
Иван Василичvivkin on Октябрь, 29, 2009 13:21 (UTC)
хуясе. ты откуда вылупился ваще?
Дядя Димаddima on Октябрь, 29, 2009 13:48 (UTC)
Вивыч, брейк :)
D.Rideraka_rider on Октябрь, 29, 2009 13:16 (UTC)
то эти API обычно очень низкоуровневые, неудачные, дырявые и не объект ориентированные
Так, насчет дырявого WinAPI сходу читаем Семена
А так, да - все правильно, только вот почему-то красивые ОО API всегда очень хреново ложатся на существующую идеологию.
Для примера - покопай Symbian API.


class CTest : public CBase 
    {   
public:
    static CTest* NewL(const TDesC& aText);
    ~CTest();
    void ConstructL(const TDesC& aText);
private :
    HBufC*   iSomeText;
    };  
_Winnie C++ Colorizer


Ведь здорово же, написал всего лишь 2 функции-утилиты и конструирование труъ объектов проходит по всем правилам платформы. Есть млн. строчек готового кода? Какие проблемы добавить эти функции к каждому классу? Ах да! На Symbian 9.2 и более ранних для объектов на стеке не вызывается деструктор в случае, когда было брошено исключение, но кому какое дело? Кругом классы - ООП.

Для кросс-платформенного софта приходится писать platform-specific обертку, а обертка над C-API пишется в разы быстрее и она в разы очевиднее, чем обертка над тем же MAC-API, который пытается навязать свою идеологию.
cd_ripercd_riper on Октябрь, 29, 2009 15:25 (UTC)
плохой пример.

на Симбе изначально кривые плюсы, без нормального RAII.

плюс ужасно криворукие и непоследовательные дизайнеры классов.
__kas__ on Октябрь, 29, 2009 20:04 (UTC)
шокирующая новость - это реальность. а эротические фантазии про с++ весде и всем не более чем фантазии
D.Rideraka_rider on Октябрь, 29, 2009 23:58 (UTC)
Согласен, потому и утрировал.
(Удалённый комментарий)
D.Rideraka_rider on Ноябрь, 2, 2009 19:30 (UTC)
Вот это новость.
Пока не укажешь флаг компиляции /EHa
Это на какой-то конкретной версии msvc, или для всех?
(Удалённый комментарий)
Дядя Димаddima on Октябрь, 29, 2009 12:00 (UTC)
Имхо, 4-й тип можно покрыть юнит тестами, только если подготовить врапперы для соответствующих системных функций и проинструктировать их, когда и что они должны возвращать. Потому что проверить, как ведет себя функция, пытающаяся открыть несуществующий файл - еще можно, а вот как проверить ветку кода с фейлом инициализации звуковой карты?