March 9, 2009
Поиск ошибок в программе
Все слышали выражение "проще переписать заново, чем исправить". Это придумали те, кто не может исправлять ошибки в программах. В чем же заключается исправление ошибки в сложном и незнакомом коде?
Я не буду рассматривать все пути (которых разумеется бесконечность, возможно, правда, счетная), а расскажу один из путей на основе своего опыта недавнего исправления ошибки. Это не будет ссылкой на формальную верефикацию программ, которая в чистом виде слишком дорога для применения по причине необходимости полной формализации системного окружения.
Первый этап - локализация ошибки. Зачастую, при сложных алгоритмах и/или при неграмотной архитектуре это самый трудоемкий этап. Что такое локализация? Это значит найти, какой код работает не так, как надо. Логичен вопрос: а как надо? Поэтому надо осознать, как должен был по задумке работать алгоритм. Это самая сложная часть, если не приходится разбираться в незнакомом и недокументированном куске программы. В этом случае приходится рисовать диаграммы и декомпозицией от большего к меньшему находить смысл всех условий и вызовов. Результатом для алгоритма будет блок-схема в терминах смысла действий (для невычислительных алгоритмов желательно описание на естественном языке, что на первой итерации упрощает анализ хода выполнения; если ошибка не будет найдена, скорее всего придется анализировать все блоки алгоритма более формально, см ниже.)
Поскольку мы рассматриваем случай, что не тот, кто написал, ищет ошибку, то можно предположить, что код работал некоторое время, а значит, появился набор данных, на котором он перестал работать так, как надо ("контрпример").
Для локализации можно сравнивать работу кода с рабочим примером по сравнению с контрпримером. Рабочий пример должен иметь минимальное отличие (неформально; чтобы ход работы алгоритма отличался минимально) от контрпримера для того, чтобы найти локализовать ошибку как можно более точно. Иногда можно локализовать ошибку просто сравнивая, какие куски кода срабатывают в каких ситуациях (ошибка в условном переходе или ошибка в самом коде).
Анализ хода выполнения выглядит так: достаточно нарисовать ход выполнения для рабочего и контрпримера в терминах построенной ранее блок-схемы (желательно на естественном языке) и сравнить полученные описания. Разница ходов выполнения и будет локализовывать с определенной детализацией ошибку.
В случае, если в обоих вариантах выполняется один и тот же код, значит ошибка носит "вычислительный характер". В этом случае анализ должен носить более формальный характер. Для этого можно разбивать алгоритм на блоки (функции) и проверять такие характеристики, как область определения и область значений каждого блока; граничные значения и поведение на них, устойчивость и другие. Для локализации ошибки нужно иметь означивания переменных для каждого стыка выделенных блоков, сравнить их с найденными значениями и это может локализовать блок (для которого уже можно повторить процедуру).
Исправление ошибки, в особенности в модуле, от которого зависят другие модули, всегда является отдельной непростой задачей, поскольку требуется обеспечить работоспособность этих модулей. Проблема обостряется, когда нет возможности легко (читай: дешево) внести изменения в зависящие модули. Но об этом позже.
Я не буду рассматривать все пути (которых разумеется бесконечность, возможно, правда, счетная), а расскажу один из путей на основе своего опыта недавнего исправления ошибки. Это не будет ссылкой на формальную верефикацию программ, которая в чистом виде слишком дорога для применения по причине необходимости полной формализации системного окружения.
Первый этап - локализация ошибки. Зачастую, при сложных алгоритмах и/или при неграмотной архитектуре это самый трудоемкий этап. Что такое локализация? Это значит найти, какой код работает не так, как надо. Логичен вопрос: а как надо? Поэтому надо осознать, как должен был по задумке работать алгоритм. Это самая сложная часть, если не приходится разбираться в незнакомом и недокументированном куске программы. В этом случае приходится рисовать диаграммы и декомпозицией от большего к меньшему находить смысл всех условий и вызовов. Результатом для алгоритма будет блок-схема в терминах смысла действий (для невычислительных алгоритмов желательно описание на естественном языке, что на первой итерации упрощает анализ хода выполнения; если ошибка не будет найдена, скорее всего придется анализировать все блоки алгоритма более формально, см ниже.)
Поскольку мы рассматриваем случай, что не тот, кто написал, ищет ошибку, то можно предположить, что код работал некоторое время, а значит, появился набор данных, на котором он перестал работать так, как надо ("контрпример").
Для локализации можно сравнивать работу кода с рабочим примером по сравнению с контрпримером. Рабочий пример должен иметь минимальное отличие (неформально; чтобы ход работы алгоритма отличался минимально) от контрпримера для того, чтобы найти локализовать ошибку как можно более точно. Иногда можно локализовать ошибку просто сравнивая, какие куски кода срабатывают в каких ситуациях (ошибка в условном переходе или ошибка в самом коде).
Анализ хода выполнения выглядит так: достаточно нарисовать ход выполнения для рабочего и контрпримера в терминах построенной ранее блок-схемы (желательно на естественном языке) и сравнить полученные описания. Разница ходов выполнения и будет локализовывать с определенной детализацией ошибку.
В случае, если в обоих вариантах выполняется один и тот же код, значит ошибка носит "вычислительный характер". В этом случае анализ должен носить более формальный характер. Для этого можно разбивать алгоритм на блоки (функции) и проверять такие характеристики, как область определения и область значений каждого блока; граничные значения и поведение на них, устойчивость и другие. Для локализации ошибки нужно иметь означивания переменных для каждого стыка выделенных блоков, сравнить их с найденными значениями и это может локализовать блок (для которого уже можно повторить процедуру).
Исправление ошибки, в особенности в модуле, от которого зависят другие модули, всегда является отдельной непростой задачей, поскольку требуется обеспечить работоспособность этих модулей. Проблема обостряется, когда нет возможности легко (читай: дешево) внести изменения в зависящие модули. Но об этом позже.
Labels: bugfix, j2me, java, programming