Найдите ошибку в программе
Искать ошибки в программах — непростая задача. Здесь нет никаких готовых методик или рецептов успеха. Можно даже сказать, что это — искусство. Тем не менее есть общие советы, которые помогут вам при поиске. В статье описаны основные шаги, которые стоит предпринять, если ваша программа работает некорректно.
Шаг 1: Занесите ошибку в трекер
После выполнения всех описанных ниже шагов может так случиться, что вы будете рвать на себе волосы от безысходности, все еще сидя на работе, когда поймете, что:
- Вы забыли какую-то важную деталь об ошибке, например, в чем она заключалась.
- Вы могли делегировать ее кому-то более опытному.
Трекер поможет вам не потерять нить размышлений и о текущей проблеме, и о той, которую вы временно отложили. А если вы работаете в команде, это поможет делегировать исправление коллеге и держать все обсуждение в одном месте.
Вы должны записать в трекер следующую информацию:
- Что делал пользователь.
- Что он ожидал увидеть.
- Что случилось на самом деле.
Шаг 3: Найдите строку, в которой проявляется ошибка
Если ошибка вызывает падение программы, попробуйте запустить её в IDE под отладчиком и посмотрите, на какой строчке кода она остановится. Совершенно необязательно, что ошибка будет именно в этой строке (см. следующий шаг), но, по крайней мере, это может дать вам информацию о природе бага.
Шаг 4: Найдите точную строку, в которой появилась ошибка
Как только вы найдете строку, в которой проявляется ошибка, вы можете пройти назад по коду, чтобы найти, где она содержится. Иногда это может быть одна и та же строка. Но чаще всего вы обнаружите, что строка, на которой упала программа, ни при чем, а причина ошибки — в неправильных данных, которые появились ранее.
Если вы отслеживаете выполнение программы в отладчике, то вы можете пройтись назад по стектрейсу, чтобы найти ошибку. Если вы находитесь внутри функции, вызванной внутри другой функции, вызванной внутри другой функции, то стектрейс покажет список функций до самой точки входа в программу (функции main() ). Если ошибка случилась где-то в подключаемой библиотеке, предположите, что ошибка все-таки в вашей программе — это случается гораздо чаще. Найдите по стектрейсу, откуда в вашем коде вызывается библиотечная функция, и продолжайте искать.
Шаг 5: Выясните природу ошибки
Ошибки могут проявлять себя по-разному, но большинство из них можно отнести к той или иной категории. Вот наиболее частые.
Если ваша ошибка не похожа на описанные выше, или вы не можете найти строку, в которой она появилась, переходите к следующему шагу.
Шаг 6: Метод исключения
Если вы не можете найти строку с ошибкой, попробуйте или отключать (комментировать) блоки кода до тех пор, пока ошибка не пропадет, или, используя фреймворк для юнит-тестов, изолируйте отдельные методы и вызывайте их с теми же параметрами, что и в реальном коде.
Попробуйте отключать компоненты системы один за другим, пока не найдете минимальную конфигурацию, которая будет работать. Затем подключайте их обратно по одному, пока ошибка не вернется. Таким образом вы вернетесь на шаг 3.
Шаг 7: Логгируйте все подряд и анализируйте журнал
Ваша задача состоит в том, чтобы вернуться к шагу 3, обнаружив, где проявляется ошибка. Также это именно тот случай, когда стоит использовать сторонние библиотеки для более тщательного логгирования.
Шаг 8: Исключите влияние железа или платформы
Замените оперативную память, жесткие диски, поменяйте сервер или рабочую станцию. Установите обновления, удалите обновления. Если ошибка пропадет, то причиной было железо, ОС или среда. Вы можете по желанию попробовать этот шаг раньше, так как неполадки в железе часто маскируют ошибки в ПО.
Если ваша программа работает по сети, проверьте свитч, замените кабель или запустите программу в другой сети.
Ради интереса, переключите кабель питания в другую розетку или к другому ИБП. Безумно? Почему бы не попробовать?
Если у вас возникает одна и та же ошибка вне зависимости от среды, то она в вашем коде.
Шаг 9: Обратите внимание на совпадения
- Ошибка появляется всегда в одно и то же время? Проверьте задачи, выполняющиеся по расписанию.
- Ошибка всегда проявляется вместе с чем-то еще, насколько абсурдной ни была бы эта связь? Обращайте внимание на каждую деталь. На каждую. Например, проявляется ли ошибка, когда включен кондиционер? Возможно, из-за этого падает напряжение в сети, что вызывает странные эффекты в железе.
- Есть ли что-то общее у пользователей программы, даже не связанное с ПО? Например, географическое положение (так был найден легендарный баг с письмом за 500 миль).
- Ошибка проявляется, когда другой процесс забирает достаточно большое количество памяти или ресурсов процессора? (Я однажды нашел в этом причину раздражающей проблемы «no trusted connection» с SQL-сервером).
Шаг 10: Обратитесь в техподдержку
Наконец, пора попросить помощи у того, кто знает больше, чем вы. Для этого у вас должно быть хотя бы примерное понимание того, где находится ошибка — в железе, базе данных, компиляторе. Прежде чем писать письмо разработчикам, попробуйте задать вопрос на профильном форуме.
Ошибки есть в операционных системах, компиляторах, фреймворках и библиотеках, и ваша программа может быть действительно корректна. Но шансы привлечь внимание разработчика к этим ошибкам невелики, если вы не сможете предоставить подробный алгоритм их воспроизведения. Дружелюбный разработчик может помочь вам в этом, но чаще всего, если проблему сложно воспроизвести вас просто проигнорируют. К сожалению, это значит, что нужно приложить больше усилий при составлении багрепорта.
Полезные советы (когда ничего не помогает)
Что вам точно не поможет
- Паника
Не надо сразу палить из пушки по воробьям. Некоторые менеджеры начинают паниковать и сразу откатываться, перезагружать сервера и т. п. в надежде, что что-нибудь из этого исправит проблему. Это никогда не работает. Кроме того, это создает еще больше хаоса и увеличивает время, необходимое для поиска ошибки. Делайте только один шаг за раз. Изучите результат. Обдумайте его, а затем переходите к следующей гипотезе. - «Хелп, плиииз!»
Когда вы обращаетесь на форум за советом, вы как минимум должны уже выполнить шаг 3. Никто не захочет или не сможет вам помочь, если вы не предоставите подробное описание проблемы, включая информацию об ОС, железе и участок проблемного кода. Создавайте тему только тогда, когда можете все подробно описать, и придумайте информативное название для нее. - Переход на личности
Если вы думаете, что в ошибке виноват кто-то другой, постарайтесь по крайней мере говорить с ним вежливо. Оскорбления, крики и паника не помогут человеку решить проблему. Даже если у вас в команде не в почете демократия, крики и применение грубой силы не заставят исправления магическим образом появиться.
Ошибка, которую я недавно исправил
Это была загадочная проблема с дублирующимися именами генерируемых файлов. Дальнейшая проверка показала, что у файлов различное содержание. Это было странно, поскольку имена файлов включали дату и время создания в формате yyMMddhhmmss . Шаг 9, совпадения: первый файл был создан в полпятого утра, дубликат генерировался в полпятого вечера того же дня. Совпадение? Нет, поскольку hh в строке формата — это 12-часовой формат времени. Вот оно что! Поменял формат на yyMMddHHmmss , и ошибка исчезла.
На обработку поступает положительное целое число, не превышающее 10 9 . Нужно написать программу, которая выводит на экран сумму цифр этого числа, меньших 7. Если в числе нет цифр, меньших 7, требуется на экран вывести 0. Программист написал программу неправильно. Ниже эта программа для Вашего удобства приведена на пяти языках программирования.
var N, digit, sum: longint;
begin
readln(N);
sum := 0;
while N > 0 do
begin
digit := N mod 10;
if digit < 7 then
sum := sum + 1;
N := N div 10;
end;
writeln(digit)
end.
Источник: демоверсия ФИПИ по информатике и ИКТ 2016-го года.
В решении задания есть видеоразборПервым делом смотрим в условие задания, а именно, как должна работать программа без ошибок. Программа должна выводить сумму цифр числа, меньших 7. Выполним задания последовательно.
1. Что выведет программа при вводе числа 456.
readln(N);
sum := 0;
while N > 0 do
begin
digit := N mod 10;
if digit < 7 then
sum := sum + 1;
N := N div 10;
end;
writeln(digit)
Первая строка — N = 456
Вторая строка — sum = 0
- digit := 456 mod 10 = 6
- 6 < 7 — условие выполняется, следовательно sum := sum + 1 = 0 + 1 = 1
- N := N div 10 = 456 div 10 = 45
- digit := 45 mod 10 = 5
- 5 < 7 — условие выполняется, следовательно sum := sum + 1 = 1 + 1 = 2
- N := N div 10 = 45 div 10 = 4
- digit := 4 mod 10 = 4
- 4 < 7 — условие выполняется, следовательно sum := sum + 1 = 2 + 1 = 3
- N := N div 10 = 4 div 10 = 0
writeln(digit) — digit в последней итерации равна 4, то есть выводится 4.
Ответ: 4
2. Приведите пример такого трёхзначного числа, при вводе которого программа выдаёт верный ответ.
Пожалуй, самая сложная часть задания. Сначала давайте определим, что вообще выводит неверно написанная программа. Как мы видим, выводится значение digit. В цикле операция digit := N mod 10; выполняется каждый повтор цикла, в последнем повторе переменная digit равна первому (старшему) разряду числа. Проще говоря, мы ввели 456 — программа вывела 4, введём 389 — программа выведет 3.
Требовалось написать программу, которая выводит сумму цифр числа, меньших 7, то есть мы должны подобрать такое трёхзначное число, чтобы старший разряд этого числа был равен сумме цифр числа, которые меньше 7. Примером может быть, к примеру, число 936 — сумма разрядов, меньших 7 равна 9-ти, на экран выводится старший разряд, то есть 9.
Ответ: 936
3. Найдите все ошибки в этой программе.
Ошибка 1. Каждый повтор цикла при выполнении условия к переменной sum прибавляется единица. Программа должна искать сумму разрядов, а не их количество, то есть к переменной sum должна прибавляться переменная digit. Значит строку
нужно заменить на
sum := sum + digit;
Ошибка 2. Программа выводит значение переменной digit, должна же выводить значение переменной sum. Заменим строку
Ответ: sum := sum + 1; -> sum := sum + digit; и writeln(digit) -> writeln(sum)
Для того, чтобы найти ошибку, нужно поставить в соответствие друг другу все части условного оператора if и else.
Помним, что часть else относится к ближайшему if. При этом наличие части else не обязательно.
Кроме того, часто присутствует ошибка при вводе или выводе. Обязательно нужно проверить, та ли информация выводится на экран.
Особого внимания требует инициализация переменных.
Формат книги не позволяет рассмотреть все основные типы задач 2 части, рассмотрим лишь те, которые встречались на проверочных и экзаменационных работах последних двух лет.
На обработку поступает положительное целое число, не превышающее 10 9 . Нужно написать программу, которая выводит на экран сумму цифр этого числа, меньших 7. Если в числе нет цифр, меньших 7, требуется на экран вывести 0. Программист написал программу неправильно. Ниже эта программа для Вашего удобства приведена на пяти языках программирования.
Бейсик
Python
INPUT N
WHILE N > 0
DIGIT = N MOD 10
IF DIGIT < 7 THEN
END IF
WEND
Паскаль
Алгоритмический язык
begin
readln(N);
while N > 0 do
begin
digit := N mod 10;
if digit < 7 then
N := N div 10;
end;
writeln(digit)
нач
цел N, digit, sum
ввод N
нц пока N > 0
если digit < 7 то
все
кц
вывод digit
Си
int main()
int N, digit, sum;
while (N > 0)
if (digit < 7)
Последовательно выполните следующее.
1. Напишите, что выведет эта программа при вводе числа 456.
2. Приведите пример такого трёхзначного числа, при вводе которого программа выдаёт верный ответ.
3. Найдите все ошибки в этой программе (их может быть одна или несколько). Известно, что каждая ошибка затрагивает только одну строку и может быть исправлена без изменения других строк. Для каждой ошибки:
1) выпишите строку, в которой сделана ошибка;
2) укажите, как исправить ошибку, т.е. приведите правильный вариант строки.
Достаточно указать ошибки и способ их исправления для одного языка программирования. Обратите внимание, что требуется найти ошибки в имеющейся программе, а не написать свою, возможно, использующую другой алгоритм решения. Исправление ошибки должно затрагивать только строку, в которой находится ошибка.
Решение использует запись программы на Паскале. Допускается использование программы на любом из четырёх других языков.
1. Программа выведет число 4.
2. Пример числа, при вводе которого программа выдаёт верный ответ: 835.
Программа работает неправильно из-за неверной выводимой на экран переменной и неверного увеличения суммы. Соответственно, программа будет работать верно, если в числе старшая цифра (крайняя левая) равна сумме цифр, меньших 7.
3. В программе есть две ошибки.
Первая ошибка. Неверное увеличение суммы.
Строка с ошибкой:
sum := sum + digit;
Вторая ошибка. Неверный вывод ответа на экран.
Строка с ошибкой:
Для заданного положительного вещественного числа A необходимо найти максимальное целое число K, при котором выполняется неравенство
(при K = 0 сумма считается равной 0).
Для решения этой задачи ученик написал такую программу.
Бейсик
Python
DIM K AS INTEGER
INPUT A
WHILE S < A
WEND
PRINT K
Алгоритмический язык
Паскаль
нач
вещ a, s
цел k
ввод a
нц пока s<a
кц
вывод k
k: integer;
begin
read(a);
while s<a do begin
end;
write(k);
Си
int main()
double a, s;
int k;
while (s<a)
return 0;
Последовательно выполните следующее.
1. Напишите, что выведет эта программа при вводе числа 1.2.
2. Приведите пример числа, при вводе которого программа даст верный ответ.
3. Найдите в программе все ошибки (их может быть одна или несколько).
Для каждой ошибки выпишите строку, в которой она допущена, и приведите эту же строку в исправленном виде.
Обратите внимание: вам нужно исправить приведённую программу, а не написать свою. Вы можете только исправлять ошибочные строки; удалять строки или добавлять новые строки нельзя. Постарайтесь также не внести новые ошибки – за это оценка снижается.
Решение использует запись программы на Паскале. Допускается использование программы на других языках.
1. При вводе числа 1.2 программа выведет число 2.
2. Примеры чисел, при вводе которых программа выводит верный ответ: 1.6, 2.05.
Программа содержит две ошибки, одна из которых приводит к увеличению ответа, другая – к уменьшению.
В некоторых случаях эти ошибки компенсируют друг друга, и ответ оказывается правильным. Это происходит, если значение A попадает в один из следующих диапазонов: 1.5 < A < 1.83, 2 < A < 2.08.
3. Программа содержит две ошибки.
1) Неверная инициализация. Начальное значение S должно быть равно нулю.
В приведённом варианте вычисленная сумма оказывается на 1 больше правильного значения.
Строка с ошибкой:
2) Неверное определение ответа. Приведённая программа находит не максимальное K, при котором выполняется неравенство, а минимальное, при котором оно не выполняется, то есть увеличивает верное значение на 1.
Кроме того, использованный порядок действий в цикле (увеличение K после увеличения S) приводит к увеличению ещё на 1. Это можно было бы исправить, изменив порядок действий в цикле и уменьшив K после завершения цикла, но эти действия не разрешены по условию задачи.
Поэтому для исправления ошибки можно просто скорректировать значение при выводе.
Достаточно часто в литературе мне попадались описания ошибок и даже классификации их по типам.
Хотя, признаться, у меня не получается толком вспомнить ни одного случая, когда мне бы помогло знание того, к какому именно типу относится конкретная ошибка. Разве что уже после выяснения причин, для обьяснения их окружающим.
А вот как люди вычисляли место и докапывались до сути ошибки — мне всегда было интересно.
Сведения о системе и ошибке
С компьютера на ПЛК подаются уставки (времена, флаги режимов) и команды на устройство.
Из ПЛК на компьютер выдаются сигналы статуса устройства и времени до конца команды на это устройство. Сигналы пакуются в слова, для минимизации объемов приема и передачи.
Из ПЛК на устройство выдаются команды.
Устройство выдает на ПЛК свои статусы.
Изначально все работало, но через какое-то время при подаче команд, статусы на компьютере в SCADA начали моргать не по делу и вообще вести себя крайне недружелюбно. Причем только в одном месте, на одном объекте.
Но «танцы с саблями» появлялись стабильно, при каждой команде, что очень порадовало.
Поиск
Цифрами в круглых скобках показаны места проверок, соответствующие цифрам на схеме ниже.
Принципов и зависимостей моргания понять сходу не удается. Возможно из-за усталости, возможно по причине отсутствия таковых.
Было установлено, что ошибка присутствует не только в SCADA (1) (там, где она собственно была обнаружена), но и в OPC сервере (2).
Дальнейший разбор показал, что ошибка присутствует и в ПЛК, как минимум в слове, формируемом для компьютера (3).
Проверка наличия ошибки в статусе, приходящем от устройства – отметает устройство, как возможный источник проблемы.
Изменения статуса от устройства вручную ничего не меняют. При изменении статуса от устройства форсированием (принудительно, постоянно) ошибка все равно сохраняется.
Соответственно это и не импульсы, которых не видно при мониторинге (4).
Сравнение с кодами других объектов, на которых этой ошибки нет – различий не выдает. Полная идентичность. Вероятность ошибки в программе ПЛК уменьшается (5).
Отключается компьютер, как возможный источник ошибки, записывающий что-то в данную область памяти. Ошибка сохраняется. Вероятность ошибки из-за компьютера стремится к нулю. Соответственно, не смотря ни на что, проблема все-таки в ПЛК (6).
Ремарка: Также можно было, отключив ПЛК, руками менять статусы в OPC. Но такой вариант был сложнее технически, а в целом эти две проверки практически равнозначны.
Перенесение слова статуса, передаваемого из ПЛК на компьютер, в другую область памяти ПЛК ничего не дает. Ошибка сохраняется.
Перенесение куска кода с ошибкой в другой блок (условно функцию) тоже не влияет на ошибку. Соответственно вероятность того, что это влияет какая-то еще посторонняя команда, пишущая туда-же что-то свое – незначительна. Дело в этом куске.
- Подача команды (без нее не понятно как проверять).
- Таймер, с которого берется время до сброса команды.
- Формирование слова на компьютере из статуса и времени до сброса команды.
Удаляется таймер. Команда не сбрасывается, но ошибка пропадает и статусы перестают прыгать.
Взамен вставляется новый таймер. Тщательно осматривается на предмет нелепостей. Таймер самый заурядный, ничего необычного. Таких в программе еще штук 200. Но ошибка появляется (8)
- Запись статусов устройства в младший байт слова.
- Замена старшего и младшего байт местами командой SWAP_WORD (статусы переносятся в старший байт)
- По AND запись времени в младший байт слова
Вроде ничего необычного, причем полностью идентичная система работает с десятками идентичных устройств вокруг. Мозг скрипит и отказывается помогать.
- Запись времени устройства в младший байт слова.
- В промежуточной переменной статусы умножаются на 256, сдвигаясь в старший байт слова.
- По OR производится запись статусов в старший байт слова.
После разбора — ситуация становится окончательно понятной.
Причина ошибки:
Операторы увеличили стандартное время таймаута с 1,5 до 10 минут.
И если 1,5 минуты это 90 секунд, то 10 минут это 600 секунд.
600 секунд не влезали в младший байт (максимум 256), и часть времени писалась в старший.
Суть последней проверки:
При записи сначала времени, и лишь затем статуса – статус забивал биты переполнения, приходившие от значения времени. А при обратной последовательности команд, время наоборот забивало своими битами статус.
Решение:
Время и статусы были разбиты на 2 слова. Местным инженерам было предложено провести техобслуживание или заменить устройство с таймаутом, превышающим стандартное время более чем в 5 раз.
И они работали они долго и счастливо, и сломались в один день.
Выводы
Описанная ошибка не особо сложна, но по-моему довольно симпатична с точки зрения показательности.
По сути не очень важно, где именно ищется ошибка — в электронике, в ПЛК, на компьютере или где-то еще. Общие принципы всегда примерно одинаковы:
- По максимуму собрать информацию о проблеме — где и как она проявляется. Осциллографы, снифферы, утилиты от Русиновича, логи, градусники, в общем всё, что можно использовать в данном случае. Не зависит ли она от времени года, прихода уборщицы, или барометрического давления.
- Вывести из-под подозрения как можно большую часть. Перерезая дорожки на печатных платах, отключая теги, выводя из системы отдельные компьютеры. Хуже, если есть какие-нибудь обратные связи и прочие хендшейки. Тогда можно попытаться либо организовать проверку приняв во внимание отсутствие части системы, либо пробовать проэмулировать часть, например искусственно подавая сигнал на вход обратной связи. В общем думать.
- По возможности доводить каждую проверку до конца, даже если вдруг появилась «мысль!». Потому-что «мысль!» может и не сработать (и до обидного часто не срабатывает), а результатов проверки лишаешься.
- В оставшемся куске — менять всё, что вызывает подозрения. Если это ПО — попробовать переустановить или заменить на аналогичное. В принципе есть вариант начать с этого пункта. Лично видел инженера, при ремонте платы на 40-50 микросхем К155 серии, выкусившего их все и впаявшего новые. Но это на мой взгляд скорее пример того, как не надо делать. Потому-что даже если все заработает, конкретики не получишь. Тем более, что в описанном случае этот вариант не прошел и неисправность сохранилась. В общем — я этого не говорил.
Хотя разумеется рецепты для одних ситуаций могут быть совершенно бесполезны по отношению другим.
Но у всех ошибок есть конкретная причина, и эту причину всегда можно выяснить.
Например как-то причиной был тракторист с бороной, проехавшийся над кабелем. И хотя сложностей в ее устранении не возникло, их хватило в процессе написания рекомендации, для избежания повторов…
Извиняюсь за вероятную сложность чтения.
Тема не очень художественная, плюс к этому попытался сократить, пропуская не очень существенные моменты, вроде вынужденного отказа от BreakPoint'ов, из-за цикличности выполнения программы в ПЛК и наличия таймера.
Читайте также: