The Russian Electronic Developer Magazine | |
Русский электронный журнал разработчика | |
Евгений Коцюба
(Evgeny Kotsuba <laser.nictl@g23.relcom.ru>)
О чем статья.
Что такое тяжелые ошибки
Логические ошибки
Тяжелые ошибки
Ошибки компилятора
Демонстрация у заказчика
Повторите ошибку
Добейтесь точного воспроизведения на своем компьютере
Ловись, рыбка большая...
Небольшое отступление для чайников об обычных методах отладки
Оптимизатор и отладчик
Закон больших чисел
Машина железная - пусть думает
Деление интервала пополам
В пределе
А если это ошибка компилятора?
Логические ошибки - это ошибки алгоритма. Например, неправильно выбранные формула, метод расчета, модель данных. Бороться с ними можно только одним способом - выбирать правильный алгоритм решения поставленной задачи. Хотя часто сама постановка отсутствует. Точнее "заказчик" не может четко поставить задачу. В этом случае разработчик-программист сам явно или неявно "ставит" задачу. Необходимо все время быть начеку, ибо никто за вас логические ошибки не найдет. В справочниках и учебниках могут опечатки. Ваш шеф, если он у вас есть, даст вам не ту формулу и т.п.
Вот вам пример из жизни. Молодой ученый занимался исследованиями динамического хаоса в химических реакциях, естественно, при помощи математического моделирования на компьютере. Писались статьи с картинками распределения каких-то частиц, полученные принтскрином экрана, делались умные выводы. При ближайшем рассмотрении оказалось, что и формула в программе немного не та, и вообще, на экран выводился только младший байт двухбайтного числа...Тяжелые ошибки - это ошибки, которые по разным причинам трудно или обнаружить, или понять, где находится их источник. Они не обнаруживаются с помощью обычных методов отладки, а после обнаружения такая ошибка может быть отнесена либо к обыкновенной ошибке, либо логической, либо ошибке компилятора.
У Микрософта вроде бы не так плохо. Единственная проблема (компилятора) в версиях с 6.0 до 8.0 3) заключалась в опции полной оптимизации на скорость, про что в хелпе предупреждается, что эта опция оптимизации "почти всегда" (лихо, не правда ли?) сгенерирует работающий код. На практике "почти всегда" превратилось в "при заказчике всегда не", что привело к отказу от оптимизации, да и выигрыш от нее при правильном написании программы был невелик. Ошибки оптимизации DOS программы проявлялись при этом элементарно: при компиляции с оптимизацией программа висла в определенных местах, при компиляции без оптимизации все работало.
У меня был случай. Заказчик пожелал увидеть круг. Большой. А я отлаживался все больше на дугах, отрезках и маленьких кружках. Нет, с кругами я тоже дело имел, но давно. С тех пор много улучшений было сделано... И большие круги никому не были нужны... Видели б вы тот круг... Но заказчик ничего не понял, через пять минут ему был продемонстрирован этот самый круг. Из маленьких отрезков...Заказчик может нажать не на ту кнопку, или вы сами в присутствии заказчика нарушите свой стереотип последовательности действий при отладке/разработке, а может быть решите продемонстрировать выдающиеся возможности вашего чуда... Оп! Ведь только вчера работало! Ничего страшного, большинство заказчиков - чайники, и скорее всего они, утомленные вашей демонстрацией, в попытках переварить увиденное и пытаясь записать в своем талмуде последовательность "F10 - Меню, F8 - ..." они не заметят ляпа. Особенно, если с важным видом произнести при этом очередную мудреную фразу. ;-) С некоторой отличной от нуля вероятностью может оказаться, что ошибка не ваша, или не совсем ваша.
Например, более новая версия одного из русификаторов для DOS (KEYRUS) зачем-то заменяет скан-коды для символьных клавиш нулем. И если у вас в программе будет стоять одна реакция на нажатие одной определенной кнопки, а не четыре проверки на ввод разных символов (с учетом переключения регистров верхнего/нижнего и русский/латинский), то с данным русификатором вы далеко не уедете.Чтобы не хвататься за сердце после демонстрации настоящему заказчику, лучше проводить данную операцию как можно чаще. Если позволяют условия, найдите для этого компьютер, где нет среды разработки, найдите бета-тестера, чем тупее, тем лучше, и садитесь молча рядом. Берите у заказчика его данные. Жмите на все кнопки подряд и сразу. Записывайте данные на дискету и вытаскивайте ее во время чтения/записи. Посадите жену за клавиатуру. Измывайтесь, как можете. Находите и методично истребляйте ошибки, и тогда при заказчике все будет нормально. Или не так страшно.
Одним из формальных методов борьбы с глюками у заказчика нынче является использование портативных компьютеров. Вы заявляетесь к заказчику или на презентацию со своим компьютером под мышкой, который можно заранее настроить и проверить. Тем не менее даже это может не помочь. Классический пример этому - презентация Б.Гейтсом "Windows-98" на выставке Comdex 98.
Ну а если ошибка не повторяется? Она обязана повторятся! Точнее, она обязана повторятся, если не меняются входные данные. А входные данные могут меняться только в том случае, если вы имеете дело с обработкой данных от внешних устройств (через порты ввода - вывода и т.п.) или же у вас где-то есть привязка ко времени суток. В этом случае для обеспечения детерминированности работы программы необходимо принять соответствующие меры. Данные от устройств ввода можно с помощью программной заглушки либо имитировать, либо читать из кольцевого буфера, в который помещать отдельно записанную реальную последовательность сигналов. Во многих случаях это может помочь, хотя при этом далеко не уйдешь от компьютера заказчика (объекта исследования или автоматизации).
Да, чуть не забыл. Помимо гипотетического случая работы в условиях повышенной радиации, возможен тривиальный вариант со сбоями памяти. Для этого не обязательно компьютер должен быть левой сборки. Он может быть и старым; и симмы с кабелями могут вываливаться; вентиляторы останавливаться, а на земле сидеть 220В; порты частично сгореть, так что мышь работает, а устройство нет; самсунговский винт может иметь привычку останавливаться только после диск доктора с последующим C-A-D...В тяжелых случаях для достижения полной детерминированности требуется полная трассировка с записью событий (например - время,номер изменившегося параметра, значение). При этом, если мы имеем дело с быстрыми процессами, обычно возникает проблема с ограниченностью оперативной памяти и "заиканием" во время записи на диск.
Еще один аспект связан с многозадачностью. Скажу честно, я не знаю как
тут правильно нужно вести отладку. Единственное, что можно сделать, как
мне представляется, если есть предположение о возникновении тяжелой ошибки,
- перейти от многозадачности к однозадачности. Только в этом случае можно
обеспечить полную детерминированность, повторяемость и строгое последовательное
выполнение задачи.
Однако однозадачность вовсе не означает полный отказ от процессов и/или
нитей (threads). Просто вы будете отлаживать и ловить ошибку только в одной
(одном) из них. Например, пользовательский интерфейс (напр., обработка
меню) может у вас работать в одной нитке, а обработка данных - в другой,
при этом первая нитка может только запускать вторую. Тогда во второй нитке
вы имеете чистую однозадачность с последовательным выполнением.
Например, на программу для DOS, скомпилированную MSC c библиотеками с использованием (в случае необходимости) эмулятора сопроцессора совершенно невероятно действует установка в сетапе, что сопроцессор отсутствует, в то время, когда он есть. Программа может проработать довольно долго, а потом, при повторном выполнении того же самого участка, зависнуть.Дальше смотрим на софт. Операционная система, драйверы, конфиги, автоэкзеки, инишники...
#define DEBUG 1 .... #if DEBUG printf( "Отладочная печать X=%i,Y=%i,Z=%i", x, y, z); #endifкогда отладка закончена вы установите DEBUG в 0
int iDebug = 0; .... if( iDebug) printf( "Отладочная печать X=%i,Y=%i,Z=%i", x, y, z);В этом случае можно в процессе отладки включать и выключать выдачу промежуточных результатов, изменяя переменную iDebug.
Если мы имеем дело с циклами, а неприятности происходят где-то посередине цикла, можно сделать так:
for( i = 0; i<1000000; i++) { ... if( i>9999) printf( " X=%i,Y=%i,Z=%i", x, y, z); }Такой прием позволяет сделать ловушку для брейкпойнтов:
for( i = 0; i<1000000; i++) { ... if( i==99999) i = i; if( x==0) printf( "!!! X=0",x); y = z/x; }Вы скажете, что в отладчике можно поставить условные брейкпойнты. Можно-то можно, только если цикл длинный, вы можете просто не дождаться, когда отладчик дойдет до нужной точки.
Еще один метод отладки заключается в графическом представлении своих данных. В случае текстовых данных, как правило, ошибка сразу бросается в глаза - вы увидите какую-нибудь билеберду вместо связного текста. Однако, если все данные у вас в цифровой форме, т.е. int и float числа и структуры, из них состоящие, то часто ничего нельзя понять, пока не перейдешь к надлежащему графическому представлению - тогда вместо правильных фигур вы увидите нечто абстрактное.
Однако в моем случае (IBM Visual Age C++ 3.0 for OS/2) без оптимизации ошибка не проявлялась. Конечно, можно было бы поступить, как и в случае с MSC - отказаться от оптимизации на скорость. Однако в данном случае выигрыш от оптимизации был более существенный, а само приложение интерактивным и быстродействие реакции на действия пользователя было крайне важно. Кроме того, ранее ничего подобного глюкам BC или MSC за этим компилятором замечено не было.
Итак, с отладчиком и без оптимизации дефект не проявляется, а с оптимизацией в отладчике делать нечего.
Что будем делать?
Как люди раньше без отладчиков жили, не знаете? Печатали промежуточные данные. Мы люди грамотные, можем не обязательно printf'ом чистым пользоваться, можно и в файл, или окошко какое. Кстати, есть множество реализаций и методов для использования printf в PM (Presentation Manager - это такая издалека напоминающая Windows графическая оболочка OS/2) . Выводим, значит эти свои промежуточные данные и смотрим на них, стараясь сообразить, где и что не так.
Если же вы имеете дело с большими числами... Например десять тысяч треугольников пятьсот раз секутся плоскостью, в каждом сечении получается около тысячи отрезков, и вот в 123-м сечении у 456-го отрезка координата Y почему-то становится равной 1243.789 вместо 1243.879.
- А как ему это объяснить, где искать?
- Ну, в нашем случае все очень просто: есть два варианта одной программы,
один работает правильно, другой нет. Все входные данные одинаковые, выходные
разные. Но кроме выходных данных, есть еще и промежуточные, которые мы
и заставим компьютер сравнивать. Примерно таким образом:
/* 1- запись данных, 2 - чтение и сравнение */ #define DEBUG 1 ... FILE *fp; int i, data[10000], d; ... #if DEBUG == 1 fp = fopen("Data.dat","wb"); #elif DEBUG == 2 fp = fopen("Data.dat","rb"); #endif for( i = 0; i<10000; i++) { data[i] = .... #if DEBUG == 1 fwrite( &data[i], 1, sizeof( int), fp); #elif DEBUG == 2 fread( &d,1,sizeof(int),fp); if( d!= data[i]) printf("\nERRor in %i, Is %i, Must be %i", i, data[ i], d); #endif } #if DEBUG fclose(fp); #endifЕще раз напоминаю, что для работоспособности подобной комбинации необходимы как одинаковые входные данные, так и однозадачность исследуемого участка. DEBUG устанавливаем в 1, транслируем без оптимизации и исполняем нашу программу. Затем DEBUG устанавливаем в 2, транслируем с оптимизатором, запускаем и смотрим, где происходит неприятность.
Гораздо удобнее пользоваться методом деления интервала пополам. Устанавливаем вышеописанную конструкцию только в двух местах. Можно в начале и конце программы ;-). А затем делим интервал пополам.
STEP0 ====================================== STEP1 =G==================================B= STEP2 =G================G=================B= STEP3 =G================G=========B=======B= STEP4 =G================G====B====B=======B= STEP5 =G================G=G==B====B=======B= STEP6 =G================G=GX=B====B=======B=На условной диаграмме "G"(Good) - это место, где данные совпали, "B"(Bad) - где данные разошлись, "X" - место возникновения ошибки.
Одну из разновидностей деления интервала пополам можно назвать методом "рвать и метать", - это когда из исходного кода по очереди отбрасывается все ненужное и несущественное. Сама программа при этом сокращается до пределов, делающих ее доступной для понимания.
Затем лучше написать статью с вашим bug-report'ом в конференцию Internet, где тусуются пользователи вашего компилятора. Попросите подтвердить или опровергнуть ваше сообщение. Дальше вы можете отослать bug-report разработчикам компилятора, хотя не надейтесь, что от них прийдет подтверждение. Даже если вы свое письмо украсите серийными номерами, копиями платежек и цветной фотографией сертификата. Хотя при известной настойчивости месяцев через шесть вам может приехать письмо, в котором будет подтверждаться наличие ошибки и ее кодовое обозначение, что будет означать, что данный вопрос зарегистрирован бюрократической машиной и рано или поздно ошибка компилятора будет ликвидирована, если только работа над данным проектом не прекращена (например, все ушли на фронт, т.е. на разработку следующей версии).
Особенно смешно выглядит такая переписка, когда bug-report касается опечаток в online Help'е.
1) Была такая операционная система - предшественница
MS Windows 95/98 [Веpнуться]
2) Бала такая ньюс-гpуппа relcom.fido.su.c-c++ -
она есть и тепеpь, независимо от фидо, а из фидо в интеpнет тепеpь она
гейтиpуется как fido7.su.c-c++. И чайников в ней, кстати, стало
намного больше...
[Веpнуться]
3) Речь идет о MSC , а не о MS Visual C++.
Visual C++ уже имеет веpсии 6 и более...пpи этом внутpенние
номеpа веpсий компилятоpа у него пpодолжаются вpоде бы.
[Веpнуться]