RDM/2 The Russian Electronic Developer Magazine
RDM/2 Русский электронный журнал разработчика
ДомойОт редактораПишите намОбратная связьRU/2

Методика поиска тяжелых ошибок.

"В каждой программе есть хотя бы одна ошибка"
народная мудрость
Евгений Коцюба
(Evgeny Kotsuba <laser.nictl@g23.relcom.ru>)

О чем статья.
Что такое тяжелые ошибки
    Логические ошибки
    Тяжелые ошибки
    Ошибки компилятора
Демонстрация у заказчика
Повторите ошибку
Добейтесь точного воспроизведения на своем компьютере
Ловись, рыбка большая...
Небольшое отступление для чайников об обычных методах отладки
Оптимизатор и отладчик
Закон больших чисел
Машина железная - пусть думает
Деление интервала пополам
    В пределе
А если это ошибка компилятора?

О чем статья.

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

Что такое тяжелые ошибки

Ошибки бывают обыкновенными, логическими и тяжелыми. Обыкновенные ошибки обычно являются ошибками кодировки или описками, прошедшими через компилятор. Проявляются они чаще всего достаточно разумным образом, в виде явного несоответствия выходных данных программы тому, что по мнению разработчика должно быть на выходе.
Отлавливаются обыкновенные ошибки, как правило, в процессе разработки при помощи отладчика и тестовых входных данных, для которых заранее известно, что должно быть на выходе. Небольшая часть остается после разработки и проявляется в процессе эксплуатации в виде багов, глюков и "соплей". Баг, как правило, - ошибка, которая точно повторяется в определенном месте после определенных действий. Глюк - это последствия багов, при которых программа в недетерминированные моменты времени начинает вести себя непредсказуемым образом. Часто это является последствием порчи памяти, что вполне может происходить и в защищенном режиме, когда программа портит "свою" память. При обращении к "чужой" памяти выскакивает в Win 3.111) "генерал П.Фолт", в русском Win95 невразумительное "Программа совершила недопустимую операцию и будет закрыта" или "Memory violation error" в OS/2. В реальном режиме программа продолжит работу, но, скорей всего, через некоторое время наглухо повиснет.
И, наконец, "сопли" - это некритические ошибки, которые не ведут к сколько-нибудь серьезным проблемам, характеризующее скорее тщательность разработки. Как насморк - неприятно, но жить можно. Обычно "сопли" заметны в огрехах пользовательского интерфейса, однако "внутри" их значительно больше.
 

Логические ошибки - это ошибки алгоритма. Например, неправильно выбранные формула, метод расчета, модель данных. Бороться с ними можно только одним способом - выбирать правильный алгоритм решения поставленной задачи. Хотя часто сама постановка отсутствует. Точнее "заказчик" не может четко поставить задачу. В этом случае разработчик-программист сам явно или неявно "ставит" задачу. Необходимо все время быть начеку, ибо никто за вас логические ошибки не найдет. В справочниках и учебниках могут опечатки. Ваш шеф, если он у вас есть, даст вам не ту формулу и т.п.

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

Ошибки компилятора

Вообще говоря, компилятор обязан работать правильно. Иначе он уже будет называться не компилятором, а глюкогенератором. Однако "в каждой программе есть ...".
Да простят меня поклонники Borland, но мне лично никак не понять их привязанности. Бесконечные новые версии, в каждой из которых свои причуды, в том числе и компилятора, ...Релкомовско-фидошная конференция SU.С-С++2), хором отвечающая очередному бедолаге, вставшему на грабли, что это новый глюк новой версии "Багланда".

У Микрософта вроде бы не так плохо. Единственная проблема (компилятора) в версиях с 6.0 до 8.0 3) заключалась в опции полной оптимизации на скорость, про что в хелпе предупреждается, что эта опция оптимизации "почти всегда" (лихо, не правда ли?) сгенерирует работающий код. На практике "почти всегда" превратилось в "при заказчике всегда не", что привело к отказу от оптимизации, да и выигрыш от нее при правильном написании программы был невелик. Ошибки оптимизации DOS программы проявлялись при этом элементарно: при компиляции с оптимизацией программа висла в определенных местах, при компиляции без оптимизации все работало.

Демонстрация у заказчика

Как правило, большая часть ошибок, не отловленных в процессе разработки, проявляется во время демонстрации программы заказчику (начальнику, шефу). Этот эффект известен многим. Еще больше ошибок вылезет во время демонстрации у заказчика, на его компьютере. Это не закон подлости, как многие считают, а вполне объяснимый факт.
Множество факторов, на которые обычно не обращают или не принимают во внимание, изменяется при переходе от рабочей среды разработки к рабочей среде заказчика. Это и драйвера, и быстродействие процессора, и версия ОС и т.д. Самое главное же, что входные данные заказчика могут оказаться совсем другими, чем те, на которых вы отлаживались.
У меня был случай. Заказчик пожелал увидеть круг. Большой. А я отлаживался все больше на дугах, отрезках и маленьких кружках. Нет, с кругами я тоже дело имел, но давно. С тех пор много улучшений было сделано... И большие круги никому не были нужны... Видели б вы тот круг... Но заказчик ничего не понял, через пять минут ему был продемонстрирован этот самый круг. Из маленьких отрезков...
Заказчик может нажать не на ту кнопку, или вы сами в присутствии заказчика нарушите свой стереотип последовательности действий при отладке/разработке, а может быть решите продемонстрировать выдающиеся возможности вашего чуда... Оп! Ведь только вчера работало! Ничего страшного, большинство заказчиков - чайники, и скорее всего они, утомленные вашей демонстрацией, в попытках переварить увиденное и пытаясь записать в своем талмуде последовательность "F10 - Меню, F8 - ..." они не заметят ляпа. Особенно, если с важным видом произнести при этом очередную мудреную фразу. ;-) С некоторой отличной от нуля вероятностью может оказаться, что ошибка не ваша, или не совсем ваша.
Например, более новая версия одного из русификаторов для DOS (KEYRUS) зачем-то заменяет скан-коды для символьных клавиш нулем. И если у вас в программе будет стоять одна реакция на нажатие одной определенной кнопки, а не четыре проверки на ввод разных символов (с учетом переключения регистров верхнего/нижнего и русский/латинский), то с данным русификатором вы далеко не уедете.
Чтобы не хвататься за сердце после демонстрации настоящему заказчику, лучше проводить данную операцию как можно чаще. Если позволяют условия, найдите для этого компьютер, где нет среды разработки, найдите бета-тестера, чем тупее, тем лучше, и садитесь молча рядом. Берите у заказчика его данные. Жмите на все кнопки подряд и сразу. Записывайте данные на дискету и вытаскивайте ее во время чтения/записи. Посадите жену за клавиатуру. Измывайтесь, как можете. Находите и методично истребляйте ошибки, и тогда при заказчике все будет нормально. Или не так страшно.

Одним из формальных методов борьбы с глюками у заказчика нынче является использование портативных компьютеров. Вы заявляетесь к заказчику или на презентацию со своим компьютером под мышкой, который можно заранее настроить и проверить. Тем не менее даже это может не помочь. Классический пример этому - презентация Б.Гейтсом "Windows-98" на выставке Comdex 98.

Повторите ошибку

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

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

Да, чуть не забыл. Помимо гипотетического случая работы в условиях повышенной радиации, возможен тривиальный вариант со сбоями памяти. Для этого не обязательно компьютер должен быть левой сборки. Он может быть и старым; и симмы с кабелями могут вываливаться; вентиляторы останавливаться, а на земле сидеть 220В; порты частично сгореть, так что мышь работает, а устройство нет; самсунговский винт может иметь привычку останавливаться только после диск доктора с последующим C-A-D...
В тяжелых случаях для достижения полной детерминированности требуется полная трассировка с записью событий (например - время,номер изменившегося параметра, значение). При этом, если мы имеем дело с быстрыми процессами, обычно возникает проблема с ограниченностью оперативной памяти и "заиканием" во время записи на диск.
Следует иметь ввиду, что даже решив проблему с полной трассировкой событий, невозможно абсолютно точно повторить взаимодействие с аналоговыми устройствами, поэтому могут возникнуть проблемы с сильно нелинейными системами и т.п.

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

Добейтесь точного воспроизведения на своем компьютере

Наконец вы доставили с поля боя исходные данные и начинаете разбор полетов на своем компьютере. Исходники скопировали в надежное место? Тогда приступайте ! Ошибка повторилась? Да - идем дальше, нет - ищем в чем дело. Чем ваш компьютер может отличатся от того, где вылезла ошибка? Сначала железо: процессор, память, bios c сетапом, видеокарта.
Например, на программу для 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 числа и структуры, из них состоящие, то часто ничего нельзя понять, пока не перейдешь к надлежащему графическому представлению - тогда вместо правильных фигур вы увидите нечто абстрактное.

Оптимизатор и отладчик

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

Однако в моем случае (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" - место возникновения ошибки.

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

В пределе

В пределе вы можете дойти до одной или нескольких строчек, и не заметить, что же делается неправильно. Попросите товарища, может он заметит. Возможно проявление стереотипов восприятия вкупе с элементарной опечаткой. Типа "Я знаю, что это переменная bd", будете смотреть на "db", а увидите "bd". Если в программе есть обе переменные, то компилятор ошибку не заметит. Иногда в этом случае бывает полезно воспользоваться case-sensitive поиском. Если поиск не находит в нужном месте вашу переменную, значит, там что-то не то написано. Можете над этим смеяться, но у меня именно был именно такой случай. Десять тысяч строк делились пополам, пока не осталось двух-трех.

А если это ошибка компилятора?

Допустим, вы дошли до нескольких строчек и убедились, что виноваты вроде бы не вы. Хорошенько разглядели в отладчике, во что превращается ваш исходный код? А фиксы (заплатки) компилятора у вас распоследние? Ну тогда садитесь и пишите баг-репорт. Лучше всего, если вы сделаете короткую программу в двух вариантах, и чтобы она писала что-то вроде "NO BUG" и "BUG!!!" Например, вот так: ibmbug.zip (2K)

Затем лучше написать статью с вашим 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нуться]