То, что программа правильно откомпилировалась не гарантирует правильность реализации заложенного алгоритма работы Основной путь - проверка на тестовых примерах Однако правильность выполнения тестового примера все равно не гарантирует отсутствие ошибок При нахождении ошибок в программе необходимо вернуться к начальному редактированию текста, компилированию, сборке и т.д. Такой процесс называется отладкой (дебаггинг - ловля блох) 9 сентября 1945 г. ученые Гарвардского университета, тестирующие машину Mark II Aiken Relay Calculator, нашли мотылька, застрявшего между контактами электромеханического реле. Извлеченной насекомое было вклеено в технический дневник, с сопроводительной записью: "First actual case of bug being found". Название "баг" прижилось. В настоящее время реликвия хранится в одном из музеев США Типовые ошибки Ошибок нет лишь в абсолютно не нужной программе. Если программа заработала с первого раза, есть большая доля вероятности, что она не будет работать никогда Даже в профессиональных программных пакетах, время от времени обнаруживаются ошибки логического характера, приводящие либо к непредусмотренному поведению программы, либо к конфликтам с другими программами, либо к конфликтам в оборудовании Практически невозможно перечислить все виды ошибок, которые допускает человек при написании программы. Сейчас мы поговорим об ошибках, которые часто совершают люди, начинающие программировать на языке Си. Речь пойдет не о синтаксических ошибках, обнаружить которые помогает компилятор, а об логически ошибочных конструкциях, приводящих к неверной работе программы Использование неинициализированного указателя Характерный пример: int *pt, t; ... *pt = 13; Указателю не присвоен адрес, и он содержит случайное число. Запись по случайному адресу может испортить данные или код программы, что приведет к краху операционной системы и потребует ее перезагрузки Допустимый вариант: int *pt, t; ... pt = &t; *pt = 13; Отсутствие завершающего нулевого байта в конце строки Пример ошибочного копирования символьной строки: char *psrs = "Копируем"; char dst[15], *pdst; pdst = dst; while(*psrs !='\0') *pdst++ = *psrs++; При невнимательном копировании строк (или посимвольном программном заполнении символьного массива) не занесен завершающий нулевой байт как признак конца текста. В результате при чтении из такого массива (dst) процесс будет продолжен за пределами массива, пока случайно не попадется нулевой байт. Результат чтения при этом непредсказуем Для устранения ошибки достаточно после приведенного текста добавить оператор: *pdst = '\0'; Лишний шаг в цикле При работе с массивами следует помнить, что диапазон индексов массива простирается от 0 до (n-1), где n - объявленная размерность массива Пример лишнего повторения цикла: int a[10], i; for(i=0; i<=10; i++) a[i] = i; Правильный вариант: int a[10], i; for(i=0; i<10; i++) a[i] = i; Использование знака присваивания(=) вместо тождества(==) В ряде языков программирования проверка на равенство двух выражений осуществляется так: if (a = b) .... В Си такая конструкция также допустима, но имеет совершенно другой смысл - оператор присваивания, заключенный в скобки, рассматривается как число, равное присвоенному значению. Поэтому (a = b) всегда истинно, кроме случая b=0 Правильный Си-фрагмент проверки на тождественное равенство: if (a == b) puts("Равно"); else puts("Не равно"); Некорректное указание пути к файлу Обратной косой чертой (\) разделяются имена каталогов при записи пути к файлу. Однако в Си с нее начинается запись управляющих символов или 8-ричных и 16-ричных констант. Такое совпадение может породить трудноуловимую ошибку при записи пути к файлу как текстовой строки: fp = fopen("c:\new\text.c", "w"); Записывая подобное, программист ожидает открыть доступ к файлу text.c. Этого не произойдет. Символы \n будут восприняты как переход на новую строку, а символы \t - как знак табуляции. Именно они войдут в состав имен каталога и файла, что является недопустимым Правильный вариант задания пути - это удвоение черты: fp = fopen("c:\\new\\text.c", "w"); Пропуск оператора break в переключателе Оператор break используется в операторе switch для прекращения обработки конкретного варианта. Если в конце варианта он не записан, будут также выполнены все операторы варианта, следующего ниже по тексту Передача в функцию копии аргумента вместо его адреса Если прототип вызываемой функции содержит в качестве аргумента указатель, то при обращении к функции фактический параметр должен быть адресом фактического аргумента Если вместо адреса будет передан сам аргумент, то последствия трудно предугадать, поскольку в любом случае переданное значение будет рассматриваться как некоторый адрес int n; scanf(“%d ",n); Правильный вариант int n; scanf(“%d ",&n); Неподключение необходимых header-файлов Если все библиотечные функции, оперирующие данными с плавающей точкой (sin(), cos(), sqrt() и т.п.), возвращают неверные значения, то скорее всего программист не включил в текст программы строку: #include <math.h> При динамическом резервировании памяти функциями типа malloc() должен быть подключен файл alloc.h: #include <alloc.h> Прочие ошибки Можно привести много примеров трудноуловимых логических ошибок В частности, открытие файла в текстовом режиме может привести к невозможности последующего чтения двоичных данных Ошибочным является возврат из функции адреса ее автоматической переменной, поскольку такие переменные прекращают существование после выхода из функции Программист должен накапливать опыт и постепенно приобретать профессиональное чутье на наличие ошибки 10 крупнейших багов 1. Ошибка в программе расчета курса ракеты-носителя Mariner 1 (28 июля 1962, первый американский аппарат, направленный к Венере), которая после схода с намеченной орбиты была уничтожена над Атлантическим океаном. Из-за поломки антенны корабль потерял связь с земными службами управлениями и перешел на собственную систему пилотирования. Но эта система содержала маленький баг. В результате аппарат полетел совсем не в ту сторону и его пришлось подорвать. Причиной инцидента послужила крошечная небрежность при переводе в машинный код формулы 2. Баг, затесавшийся в канадский пакет софта, управляющего контрольной системой газопровода, вызвал произошедшую в 1982 году аварию участка Транссибирской газомагистрали, который вызвал крупнейший из когдалибо зарегистрированных на земной поверхности неядерный взрыв. Оперативники ЦРУ внедрили баг (отчет) в канадское программное обеспечение, управлявшее газовыми трубопроводами. Советская разведка получила это ПО как объект промышленного шпионажа и внедрила на Транссибирском трубопроводе. 3. Ошибка переполнения буфера в берклиевской версии Unix при вызове функции gets(). В ОС Berkeley Unix эта функция ввода/вывода не имела ограничения на максимальную длину. Эта ошибка была использована при написании самого первого в истории компьютерного вируса - червя Морриса (1988). Просочившись в Сеть, всего за один день вирус-пионер отправил в нокаут примерно одну десятую часть тогдашнего Интернета 4. Девятичасовая авария, выведшая из строя системы американской компании AT&T (1990). Причина - обновление софта для коммутаторов, обслуживающих "дальнобойные" звонки: электронные "телефонисты" тут же принялись рассылать своим соседям сигналы перезагрузки с интервалом в несколько секунд, в результате чего телефонная сеть рухнул 5. Cбой в компьютерной системе настройки ускорителя Therac-25, используемого в радиационной терапии (1985–87). Ошибка при задании программных настроек привела к неверному размещению металлического защитного экрана, поглощающего электроны высоких энергий и генерирующего рентгеновское излучение (экран был подвижным, поскольку система предусматривала режим прямого облучения пациентов электронами низких энергий). В результате многие пациенты попали под высокоэнергетический электронный пучок ускорителя, получили удар электрическим током и сильные ожоги. По крайней мере пять человек умерло позже от лучевой болезни 6. Дефект генератора ключей системы криптозащиты Kerberos - самый "долгоиграющий" баг, просуществовавший с 1988 по 1996 годы. Как выяснилось, по недосмотру программистов использованный в нем генератор случайных чисел оказался недостаточно "случайным". Предполагалось, что программа должна выбирать ключ случайным образом из многих миллиардов чисел, но генератор случайных чисел выбирал из гораздо меньшего набора численностью примерно в миллион. Как результат, в течение восьми лет любой пользователь мог без труда проникнуть в компьютерную систему, которая использовала модуль Kerberos. 7. Во время натовской операции «Буря в пустыне» батарея ракет «Пэтриот» не смогла перехватить иракскую ракету «Скад». Причиной этого стала неправильной вычисление времени в формате с плавающей запятой. Система управления ракеты имела внутренние системные часы, отсчитывающие время в десятых доля секунды. Для перевода в целые секунды компьютер просто умножал на 1/10. Однако дробь 1/10 представляется в формате с плавающей запятой с погрешностью. После четырех дней непрерывной работы расхождение системного времени с точным составило около 1/3 секунды, что привело к ошибке наведения 700 м. В результате ракета, выпущенная на перехват «Скада», угодила в барак с американскими военнослужащими, убив 28 человек. 8. Ошибка в операции деления чисел с плавающей запятой в процессорах Pentium, Intel (1993). Это серьезно подмочило репутацию Intel. В результате пришлось заменить миллионы дефектных чипов, а весь скандал обошелся Intel примерно $475 млн. 9. "The Ping of Death" (1995–96) - баг, связанный с неправильной обработкой некоторыми операционными системами слишком больших IP-пакетов (в частности, производимых командой Ping). Отсутствие проверки на ошибки при обработке IP-пакетов позволяла порушить практически любую операционную систему, отправив ей через интернет специальный пакет ("пинг"). 10. Баг в коде управляющей программы ракеты Ariane 5 (гордость стран Евросоюза 1996), вызвавший ее взрыв спустя сорок секунд после старта. Система автоподрыва ракеты сработала после остановки обоих процессоров в результате цепочки ошибок. Началом этой цепочки послужило переполнение буфера, поскольку система навигации подала недопустимо большое значение параметра горизонтальной скорости. Дело в том, что система управления Ariane 5 переделывалась из Ariane 4, а там такого большого значения не могло быть теоретически. В целях снижения нагрузки на рабочий компьютер инженеры сняли защиту от ошибок переполнения буфера в этом программном модуле, поскольку были уверены, что такого значения горизонтальной скорости не может быть в принципе В результате ошибка переполнения регистра вызвала сбой системы ориентации в пространстве, неправильную коррекцию курса и разрушение корпуса под действием аэродинамических сил. При этом дублирующий модуль вышел из строя всего лишь на 0,05 секунды позже основного. Только научное оборудование на борту ракеты стоило около $500 млн . 11. Ошибка в графическом интерфейсе программы по управлению системой лучевой терапии в панамском Институте рака (2000). Экраны, защищавшие от радиации здоровые органы, отображались на мониторе в виде прямоугольных "кирпичиков". Оказалось, что программа была чувствительна к их расположению. Программа позволяла врачу нарисовать на компьютерном экране расположение защитных металлических щитов, которые защищают тело от радиации. Но программа позволяла манипулировать только четырьмя щитами, тогда как врачи хотели задействовать пять. Они нашли способ "обхитрить" программу, если нарисовать все пять щитов в виде единого блока с дыркой посередине. Единственное, чего они не знали, что программа рассчитывает разные дозы радиации в зависимости от того, как нарисована дырка. Если рисовать ее особым образом, то устройство выдавало двойную дозу радиации. В результате три десятка пациентов получили передозировку. Не меньше восьми человек умерли от лучевой болезни (за три года скончался 21 пациент, но часть смертей могла быть вызвана прогрессирующим раком). Врачи, которые должны были вручную перепроверять расчеты программы, были осуждены за убийство.