ЭЛЕКТРОННАЯ БИБЛИОТЕКА КОАПП
Сборники Художественной, Технической, Справочной, Английской, Нормативной, Исторической, и др. литературы.



 

Часть 18

Глава 15. Отладка стандартной прикладной программы на Паскале
Отладка программы аналогична всем другим этапам реализации программы - это наполовину искусство, наполовину наука. Существуют специальные процедуры, которые можно использовать для отслеживания ошибки, однако, чтобы сократить этот процесс, требуется также
 хорошая интуиция. 
В большинстве отлаживаемых вами программ лучшее, что вы можете сделать - это быстро найти источник ошибок в исходном коде. Для этого нужно освоить соответствующие методы, а также изучить такие способы, которые позволят избежать повторного появления ошибо
к. 
В данной главе мы обсудим также различные подходы к отладке, разные типы ошибок, которые могут встречаться в программе, и предложим методы проверки программы, позволяющие убедиться в правильности ее работы. 
Мы начнем с того, что посмотрим, с чего можно начать отладку программы, которая не работает должным образом. 
Когда что-то не работает
Прежде всего не следует впадать в панику. Даже наиболее опытные программисты редко пишут программы, которые начинают работать с первого раза. 
Чтобы избежать напрасной траты времени на долгие и бесплодные поиски ошибки, постарайтесь побороть стремление случайно угадать, где находится ошибка. Лучшим методом здесь будет универсальный принцип "разделяй и властвуй". 
Нужно сделать ряд предположений, проверив каждое из них по очереди. Например, вы можете предположить: "Ошибка должна возникать перед вызовом функции xyz". Затем нужно проверить это предположение, остановив программу перед вызовом функции xyz и посмотрев,
 есть ли ошибка. Если вы обнаружите ошибку в этой точке, можно сделать следующее предположение, что ошибка возникает в программе где-то раньше. 
С другой стороны, если при вызове функции xyz все выглядит прекрасно, ваше предположение оказалось неверным. Нужно изменить это предположение на следующее: "ошибка возникает где-то после вызова функции xyz". Выполнив ряд аналогичных проверок, вы скоро на
йдете ту часть программы, где возникает ошибка. 
Это прекрасно, скажете вы, но как же определить после остановки программы, что она ведет себя правильно? Один из наилучших путей проверки поведения программы состоит в анализе значений переменных и объектов данных программы. Например, если у вас есть под
программа, очищающая массив, вы можете проверить ее работу, остановив программу после выполнения данной подпрограммы и проверив затем каждый элемент массива, чтобы убедиться, что он очищен. 
Стиль отладки
У каждого свой стиль как разработки программы, так и ее отладки. Те рекомандации по отладке, которые мы здесь приводим, являются лишь отправными пунктами, которые позволят вам сформировать свой подход. 
В многих случаях на метод отладки влияет предпологаемое использование (назначение) программы. Некоторые программы вы пишете для себя, либо они будут использованы только один или два раза для выполнения конкретной задачи. Для таких программ разносторонее 
тестирование всех их элементов было бы напрасной тратой времени, особенно, если после проверки ее выходных данных вы видите, что программа работает правильно. Для тех программ, которые предполагается распространять, или для тех, которые выполняют задачу,
 правильность которой трудно определить с помощью проверки, может оказаться желательным более строгое тестирование. 
Полное выполнение
Для простых программ лучший подход, вероятно, состоит в том, чтобы просто запустить программу и посмотреть, что получилось. Если при такой проверке будут обнаружены ошибки, вы можете "сделать шаг назад" и запустить программу с максимально простыми входны
ми данными, чтобы проверить затем ее вывод. Затем можно перейти к проверке с более сложными входными данными, и так делее, пока выходная информация не станет неверной. Это даст вам хорошее представление о том, насколько корректно работает программа. 
Последовательное тестирование
Если вы хотите полностью убедиться, что вся программа работает правильно, нужно проверить отдельные подпрограммы, а также убедиться, что программа выдает ожидаемые результаты для некоторых тестовых входных данных. Это можно сделать двумя способами: можно
 выполнить проверку каждой подпрограммы, включив ее в программу-тест, которая вызывает подпрограмму с тестовыми входными данными, или использовать отладчик для пошагового выполнения каждой подпрограммы, пока не будет выполнена вся программа. 
Типы ошибок
Ошибки можно разбить на две большие категории: специфические для используемого языка ошибки и более общие ошибки программирования, которые относятся к любому языку и операционной среде. 
По мере отладки программ вы изучите как специфические для языка ошибки, которые могут приводить к неприятностям, так и более общие программные ошибки, которые вы сделаете. Это знание можно использовать в последующем, чтобы постараться избежать повторения
 таких ошибок. Кроме того, это послужит хорошей базой для того, чтобы быстрее обнаруживать ошибки в следующих программах, которые вы будете писать. 
Здесь важно понимать, что собой представляет каждая ошибка: относится ли она к общим ошибкам или вызвана непониманием. Это улучшит ваши возможности по разработке исходного кода без ошибок. Кроме того, всегда лучше писать программу без ошибок, чем уметь б
ыстро потом их исправлять. 
Общие ошибки
В следующий примерах кратко охватываются различные типы ошибок, которые могут встречаться в ваших программах. 
Скрытые эффекты
Иногда вызов функции может приводить к неожиданным результатам: 
     program Buffers;
     uses WinCrt, Strings;
     var
        WorkBuf,
        AllCaps : PChar;
     procedure Convert(S : PChar);
     var
        Len := StrLen(s);
        StrCopy(WorkBuf, S);
        for I := 1 to Len do
            ; { ... }
     end;
     begin
        WorkBuf := 'all done';
        AllCaps := 'SNAFU';
        Convert(AllCaps);
        Wroteln(WorkBuf);
     end.
Здесь правильнее было бы использовать в функции свой собственный рабочий буфер (WorkBuf). 
Предположения об инициализации данных
Иногда вы предполагаете, что другая подпрограмма уже установила для вас какие-то значения: 
     uses Strings;
     var
        WorkBuf: PChar;
     procedure AddWorkString(S: PChar);
     begin
        StrCopy(WorkBuf, S);   { oop }
     end;
Надежнее будет записать эту подпрограмму, добавив оператор: 
        if (WorkBuf = nil) then New(WorkBuf);
Не забывайте об очистке
Этот тип ошибки может привести к тому, что ваша программа будет долго работать, но в конце-концов исчерпает динамически распределяемую область памяти и аварийно завершит работу: 
     function CrunchString(S: PChar): PChar;
     var
        Work: PChar;
     begin
        New(Work);
        StrCopy(S, Work);
        { ... }
        CrunchString := S; { работает, пока выделяется память }
     end;
"Забор и столбы"
Этот тип ошибок аналогичен следующей задачке. Сколько столбов понадобиться, чтобы постоить 100-метровую изкородь, если столбы нужно ставить через каждые 10 метров? Напрашивается ответ 10, но он неверен, так как в расчет принимается последний столб в конц
е забора. Приведем простой пример: 
     i := 1;
     while i < 10 do
     begin
        ...      { выполняется только 9 раз }
     end;
Здесь ясно видны числа 1 и 10, и вы можете подумать, что цикл будет выполняться от 1 до 10. Чтобы это действительно было так, нужно вместо < указать <=. 
Ошибки, специфические для Паскаля
Поскольку в Паскале имеются средства, обеспечивающие строгую проверку типов и проверку ошибок, то этот язык мало способствует специфическим для него ошибкам. Однако, поскольку Турбо Паскаль предоставляет вам возможность "выключать" проверку ошибок, вы мо
жете внести ошибки, которые в противном случае не возникли бы. Однако даже в Паскале есть способы этого избежать. 
Неинициализированные переменные
Турбо Паскаль не инициализирует переменные автоматически. Вы должны сделать это сами с помощью операторов присваивания или описав такие переменные в виде типизованных констант. Рассмотрим следующую программу: 
        program Test;
        uses WinCrt;
        var
                I,J,Count       : integer;
        begin
                for I := 1 to Count do begin
                 J := I*J;
                 Writeln(I:2,' ',J:4)
                end
        end.
Здесь Count будет иметь какое-то случайное значение, содержащееся в занимаемой этой переменной ячейке памяти, поэтому вы не сможете определить, сколько раз будет выполнен данный цикл. 
Кроме того, переменные, описанные внутри процедуры или функции, будут создаваться каждый раз при входе в эту подпрограмму и уничтожаться при выходе из нее. Поэтому нельзя полагать, что эти переменные в промежутке между вызовами подпрограммы сохраняют сво
е значение. 
Неправильная работа с указателями
Этот общий тип ошибок встречается при работе с указателями. Во-первых, как уже упоминалось ранее, не следует использовать их до того, как им будет присвоено значение (nil (пустое) или какое-либо другое). Как и все другие переменные или структуры данных, 
указатель не инициализируется автоматически при его описании. Ему нужно явным образом присвоить начальное значение (передав его в качестве параметра процедуре New или возможно быстрее присвоив ему значение nil). 
Во-вторых, не ссылайтесь на пустой указатель, то есть не пытайтесь обратиться к данным или структуре, на которые он указывает, если указатель имеет значение nil. Например, предположим, что у вас имеется линейный связанный список записей, и вы хотите выпо
лнить в нем поиск записи с заданным значением. Ваша программа может выглядеть следующим образом: 
     function FindNode(Head : NodePtr, Val : integer);
     var
             Temp : NodePtr;
     begin
             Temp := Head;
             while (Temp^.Key <> Val) and (Temp <> nil) do
                     Temp := Temp^.Next
             FindNode := Temp
     end { FindNode }
Если Val не равно полю Key в каком-либо из узлов связанного списка, то эта программа, когда Temp имеет значение nil, будет пытаться вычислить Temp^.Key, что приведет к непредсказуемому поведению. Каково же здесь решение? Нужно записать выражение следующи
м образом: 
while (Temp <> nil) and (Temp^.Key <> Val)
 и разрешить вычисление булевских выражений по короткой схеме (с помощью директивы Турбо Паскаля {$B-} или команды OptionsіCompiler (ПараметрыіКомпилятор) для вывода диалогового окна и установки значения Short Circuit (Вычисление по короткой схеме). Таки
м образом, если Temp не равно nil, второе условие вычисляться не будет. 
Наконец, не следует предполагать, что указатель устанавливается в значение nil только потому, что вы передаете его процедуре Dispose или FreeMem. Указатель будет иметь при этом свое исходное значение, однако память, на которую он указывает, будет теперь 
освобождена, и может использоваться для другой динамической переменной. После освобождения структуры данных указатель нужно явным образом установить в значение nil. 
Неправильное использование области действия
Паскаль позволяет вам использовать большой уровень вложенности процедур и функций, и в каждой их этих процедур и функций могут содержаться ее собственные описания. Рассмотрим следующую программу: 
     program Confused;
     uses CrtWin;
     var
             A,B :integer;
     procedure Swap(var A,B : integer);
     var
             T : integer;
     begin
             Writeln('2: A,B,T = ',A:3,B:3,' ',T);
             T := A;
             A := B;
             B := T;
             Writeln('3: A,B,T =',A:3,B:3,' ',T);
     end { Swap }
     begin { тело основной программы Confused }
             A:= 10; B := 20; T := 3-;
             Writeln('1: A,B,T = ',A:3,B:3,' ',T);
             Swap(B,A);
             Writeln('4: A,B,T = ',A:3,B:3,' ',T);
     end. { Confused }
Выводимая программой информация будет выгдялеть примерно следующим образом: 
        1: A,B,T = 10 20 30
        2: A,B,T = 20 10 22161
        3: A,B,T = 10 20 20
        4: A,B,T = 20 10 30
Все это вызвано тем, что у вас имеется две версии переменных A, B и T. В теле основной программы используются глобальные версии, в процедуре Swap - локальные версии (ее формальные параметры A и B и локальная переменная T). И что еще более запутало ситуац
ию, мы обратились с вызовом Swap(B,A), что означает, что формальный параметр A является на самом деле глобальной переменной B и наоборот. И, конечно, нет никакой связи между локальной и глобальной версией переменной T. 
Настоящей ошибки здесь нет, но проблемы могут возникнуть, когда вы будете считать, что модифицируете что-то, а на самом деле это не так. Например, переменная T в теле основной программы не изменяется, хотя вы можете предполагать, что это не так. Этот рез
ультат, обратный описанным ранее "скрытым эффектам". 
Если бы вы использовали следующее описание записи, все стало бы еще более запутанным: 
     type
        RecType = record
                A,B : integer;
        end;
     var
        A,B : integer;
        Rec : RecType;
В операторе with сслыка на A или B привела бы к ссылке на поля, а не к ссылке на переменные. 
Неправильное использование точки с запятой
Паскаль допускает использование "пустого" оператора (оператора, состоящего только из точки с запятой). Размещенная в неверном месте точка с запятой может вызвать различные проблемы. Рассмотрим следующую программу: 
     program Test;
     uses WinCrt;:
     var
             I,J : integer;
     begin
             for I := 1 to 20 do;
             begin
                     J := I*I;
                     Writeln(I:2,' ',J:4)
             end;
             Writeln('Выполнено!');
     end.
Выводом этой программы будет не список из первых 20 целых чисел и их квадратов, а просто: 
        20 400
        Выполнено!
Это вызвано тем, что оператор for I := 1 to 20 заканчивается точкой с запятой. При этом 20 раз будет выполнен пустой оператор. После этого выполняется оператор в блоке begin...end и, наконец, оператор Writeln. Чтобы исправить эту ошибку, нужно просто уст
ранить точку с запятой за ключевым словом do. 
Функция возвращает неопределенное значение
Когда вы пишете функцию, нужно убедиться, что перед тем, как фукция возвращает управление, ее имени присваивается некоторое значение. Рассмотрим следующий пример кода: 
     const
             NLMax = 100;
     type
             NumList = array[1...NLMax] of integer;
             ...
     function FindMax(List : Numlist; Count : integer) : integer;
     var
             I,MAX : integer;
     begin
            Max := List[1];
             for I := 2 to Count do
             if List[I] > Max then
             begin
                     Max := List[I];
                     FindMax := Max
             end
     end; { FindMax }
Эта функция будет прекрасно работать, если максимальным значением в List не является List[1]. В этом случае никогда не будет присвоено значение. Правильный вариант функции должен выглядеть следующим образом: 
     begin
         Max := List[1];
         for I := 2 to Count do
         if List[I] > Max then
         Max := List[I];
         FindMax := Max
     end; { FindMax }
Уменьшение значения переменных размером в байт или слово
Будьте внимательны и не уменьшайте беззнаковое скалярное значение (размером в слово или байт) при проверке на >= 0. Следующий фрагмент программы образует бесконечный цикл: 
     var
        w : word;
     begin
        w:= 5;
        while w >= 0 do
                w := w - 1;
        end.
После пятой итерации w равно 0. При следующем проходе оно будет уменьшено до значения 65535 (так как переменная размером в слово принимает значения в диапазоне от 0 до 65535), что также >= 0. В этих случаях следует использовать переменные не типа word ил
и byte, а типа integer или longint. 
Игнорирование границ и особые случаи
Заметим, что в обеих версиях функции FindMax в предыдущем разделе предполагалось, что Count >= 1. Однако в некоторых случаях значение Count может быть равно 0 (то есть список пуст). Если вы в такой ситуации вызовите функцию FindMax, она возвратит то, что
 оказалось в List[1]. Аналогично, если Count > NLMax, выполнение либо завершиться с ошибкой (если разрешена проверка границ), либо поиск максимального значения будет выполняться в ячейках памяти, не относящихся к List. 
Здесь можно предложить два решения. Одно из них состоит, конечно, в том, чтобы никогда не вызывать функцию FindMax, если Count не находится в диапазоне 1..NLMax. Это не пустое замечание. В серьезном программном обеспечении всегда определяются требования,
 которые нужно выполнять при вызове определенной программы, а затем обеспечивается удовлетворение этих требований при вызове. 
Другое решение состоит в проверке значения Count и, если оно не находится в диапазоне 1..NLMax, возврате некоторого предопределенного значения. Например, вы можете переписать тело функции FindMax следующим образом: 
     begin
        if (Count < 1) or (Count > NLMax) then
                Max := -32768
        else
        begin
           Max := List[1];
           for I := 2 to Count do
           if List[I] > Max then
              Max := List[I];
        end;
        FindMax := Max
     end; { FindMax }
Однако это приводит к следующему типу ошибок при работе на Паскале - ошибкам диапазона. 
Ошибки диапазона
По умолчанию в Турбо Паскале проверка диапазона "выключена". При этом получается более быстрый и компактный код, но в тоже время при этом вы можете сделать определенного типа ошибки, такие, как присваивание переменным значения, выходящего за их допустимы
й диапазон, или обращение к несуществующему элементу массива (как показано в приведенном выше примере). 
Первый шаг при обнаружении таких ошибок состоит во включении в программу директивы компилятора {$R+} (которая задает проверку диапазона), компиляции программы и повторном ее запуске. Если вы знаете (или догадываетесь), где содержится ошибка, можно помест
ить указанную директиву перед данной частью программы, а после нее указать директиву {$R-}, разрешив, таким образом, проверку диапазона только в той части программы, где содержится ошибка. 
Если для компиляции программы вы используете компилятор TPCW, то для обнаружения ошибки можно использовать параметр командной строки /F. Подробнее об этом рассказывается в "Руководстве пользователя" Турбо Паскаля для Windows. 
Одна из общих ошибок выхода за границы диапазона возникает при использовании для индексации массива цикла while или repeat. Предположим, например, что вы ищете элемент массива, содержащий определенное значение. Вы хотите остановиться после того, как найд
ете его, или при достижении конца массива. При нахождении элемента вы ходите возвратить его индекс, а в противном случае - 0. Ваш первый вариант может выглядеть так: 
     function FindVal(List : NumList; Count,Val :
                  integer) : integer;
     var
             I : integer;
     begin
            FindVal := 0;
             I := 1;
             while (I <= Count) and (List[I] <> Val) do
                     Inc(I);
             if I <= Count then
                     FindVal := I
     end; { FindVal }
Это прекрасно, но если Val не содержится в List, и вы используете обычное вычисление булевских выражений, здесь может возникнуть ошибка этапа выполнения. Почему? Потому что когда последний раз проверка выполняется в начале цикла while, I будет равно Coun
t + 1. Если Count = NLMax, вы выйдете за пределы List. 
Существует два способа решения этой проблемы. Один из них состоит в выключении проверки диапазна. Однако, это может привести к появлению трудноуловимых ошибок, особенно если используемый код на самом деле изменяет значение. Более подходящее решение, пока
занное ранее, состоит в вычислении булевских выражений по короткой схеме или использовании директивы Турбо Паскаля ($B-}, либо выбора команды OptionsіCompiler (ПараметрыіКомпилятор) для вывода диалогового окна Compiler Options и установки переключателя S
hort Circuit (Вычисления по короткой схеме). Таким образом, если I > Count, то выражение: 
     List[I] <> Val
 никогда не вычисляется. 
Ошибки, специфические для Ассемблера
Мы рассмотрим некоторые типичные ошибки, которые допускаются при программировании на Ассемблере, и дадим рекомендации, как можно их избежать. Этот раздел предназначен для тех, кто пользуется Турбо Ассемблером или использует встроенный Ассемблер в програм
мах Турбо Паскаля. Более подробно о встроенном Ассемблере Турбо Паскаля рассказывается в "Руководстве пользователя" Турбо Паскаля для Windows. 
Программист забывает о возврате в DOS
В Паскале программа завершается и возвращается в операционную систему DOS автоматически, когда нет больше выполняемого кода, даже если в программе отсутствует явная команда ее завершения. В языке Ассемблера это не так. Ассемблер выполняет только те дейст
вия, которые вы явно указываете. Когда вы запускаете программу, в которой отсутствует команда возврата в DOS, она просто продолжает работать до конца выполняемого кода программы и переходит в код, который находится в примыкающей памяти. 
Программист забывает об инструкции RET
Заметим, что правильный вызов подпрограммы состоит из вызова подпрограммы из другой части кода, выполнения подпрограммы и возврата из подпрограммы в вызывающую программу. Не забудьте включать в каждую подпрограмму инструкцию RET, по которой управление бу
дет передаваться в вызывающий код. При наборе программы эту директиву легко пропустить. В этом случае ее выполнение закончится ошибкой. 
Генерация неверного типа возврата
Директива PROC действует двояко. Во-первых, она определяет имя, по которому будет вызываться процедура. Во-вторых, она управляет типом (ближним или дальним) процедуры. 
Идея здесь очевидна. Инструкции RET в процедуре должны соответствовать ее типу, не правда ли? 
И да и нет. Проблема состоит в том, что возможно и часто желательно группировать отдельные подпрограммы в единую процедуру; и поскольку эти подпрограммы не имеют соответствующей директивы PROC, их команды RET соответствуют типу общей процедуры, который н
е обязательно соответствует типу каждой отдельной подпрограммы. 
Неправильный порядок операндов
Многие программисты ошибаются и изменяют порядок операндов в инструкциях процессора 8086 на обратный. Это, вероятно, связано с тем, что строка: 
        mov ax,bx
 которая означает "поместить AX в BX", читается слева направо, и многие создатели микропроцессоров строят соответствующим образом свои ассемблеры. Однако в языке Ассемблера процессора 8086 фирма Intel использовала другой подход, поэтому для нас эта строк
а означает "поместить BX в AX", что иногда приводит к путанице. 
   Программист забывает о стеке или резервирует маленький стек
В большинстве случаев не выделять явно пространство для стека, это все равно, что ходить по тонкому льду. Иногда программы, в которых не выделяется пространство для стека, будут работать, поскольку может оказаться так, что назначенный по умолчанию стек п
опадет в неиспользуемую область памяти. Но нет никакой гарантии, что такие программы будут работать при любых обстоятельствах, поскольку нет гарантии, что для стека будет доступен по крайней мере один байт. В большинстве программ для резервирования прост
ранства для стека в программе DOS может присутствовать директива .STACK, а для любой программы эта директива должна резервировать достаточное пространство, чтобы его хватило для максимальных потребностей в программе. В Турбо Паскале для Windows вы можете
 задать размер стека с помощью меню OptionsіCompiler (ПараметрыіКомпилятор). Для каждой программы нужно резервировать пространство, достаточное для использования в самом большом стеке программы. 
 Вызов подпрограммы, которая портит содержимое нужных регистров
При разработке программы на Ассемблере регистры удобно рассматривать, как локальные переменные, выделенные для использования в процедуре, с которой вы в данный момент работаете. В частности, нередко подразумевают, что при обращении к другим процедурам ре
гистры остаются неизмененными. На самом деле это не всегда так. Регистры - это глобальные переменные, и каждая процедура может сохранить или уничтожить содержимое любого из регистров. 
Ошибки при использовании условных переходов
Использование в языке Ассемблера инструкций условных переходов (JE, JNE, JC, JNC, JA, JB, JG и т.д) обеспечивает большую гибкость в программировании, но при этом также очень просто ошибиться, выбрав неверный переход. Кроме того, поскольку в языке Ассембл
ера анализ условия и переход требуют по крайней меру двух строк исходного кода (а сложных условных переходов нескольких строк), условные переходы в языке Ассемблера менее очевидны и больше способствуют ошибкам, чем соответствующие операторы Паскаля. 
Ошибки в переопределении REP в строковых инструкциях
Строковые инструкции имеют любопытное свойство: после их выполнения используемые ими указатели будут указывать на 1 байт (или 2 байта для инструкции размером в слово) от последнего обработанного адреса. Это может вызвать некоторую путаницу при повторении
 строковых инструкций, особенно в инструкциях REP SCAS и REP CMPS. 
Нулевое содержимое CX и работа с целым сегментом
Повторное выполнении любых команд обработки строк при равенстве нулю регистра CX не даст никакого результата. Это может быть удобно в том смысле, что нет необходимости проверять его на ноль перед повторным выполнением команд обработки строк. С другой сто
роны, невозможно получить доступ к каждому байту в сегменте с помощью байтовых команд обработки строк. 
Неправильная установка флага направления
При выполнении команды обработки строк связанные с ней указатели (SI, DI или оба) получают положительное или отрицательное приращение. Это зависит от состояния флага направления. 
С помощью команды CLD флаг направления может быть сброшен в 0. В этом случае при выполнении команд обработки строк указатель получает положительное приращение (смещается в сторону старших адресов). С помощью команды STD флаг направления устанавливается в
 1. В этом случае указатель получает отрицательное приращение (сдвигается в сторону младших адресов). После того, как флаг направления был установлен в определенное состояние, он будет оставаться в нем до тех пор, пока не будет выполнена еще одна команда
 CLD или STD, или пока значения флагов не будут извлечены из стека с помощью команды POPF или IRET. С одной стороны, удобно иметь возможность устанавливать флаг направления в определенное состояние только один раз, а затем выполнять последовательность ко
манд, которые должны использовать заданное направление. С другой стороны, это может привести к появлению неустойчивых и труднообнаруживаемых ошибок, в результате которых команды обработки строк работают по-разному, в зависимости от работы команд, которые
 были выполнены значительно раньше. 
   Неверное понимание повторения команд сравнения строк
Инструкция CMPS сравнивает содержимое двух областей памяти, а инструкция SCAS сравнивает содержимое накапливающего регистра с содержимым области памяти. Когда перед одной из этих инстроукций стоит префикс REPE, она выполняет сравнение, либо пока регистр 
CX не становится равным нулю, либо пока не обнаружится, что операнды не равны. Когда перед командой стоит префикс REPNE, она выполняет сравнение либо пока CX не становится равным нулю, либо пока не обнаружится что операнды равны. К несчастью, легко переп
утать, где какой префикс нужно использовать. 
Ошибки при назначении сегмента строк по умолчанию
Все строковые инструкции по умолчанию используют в качестве сегмента исходных данных (источника, если он есть) сегмент DS, а в качестве сегмента результирующих данных (приемника, если он есть) сегмент ES. Легко забыть об этом и попытаться, скажем, выполн
ить инструкцию STOSB над сегментом данных, поскольку все данные, обрабатываемые не строковыми командами, обычно находятся именно в этом сегменте. 
Неправильное преобразование из байта в слово
В общем случае, для команд обработки строк желательно использовать максимально возможный размер данных (обычно слово, а для процессора 80386 - двойное слово), поскольку с данными большего размера эти команды обычно работают быстрее. 
Однако здесь имеются две ловушки. Во-первых, преобразование из количества байт в количество слов с помощью простой команды: 
shr cx,l
 приведет к потере байта, если CX имеет нечетное значение (поскольку младший значащий бит будет сдвинут за пределы слова). 
Во-вторых, следует помнить, что команда SHR делит количество байт на два. Использование, скажем, команды STOSW с количеством байт, а не слов, может уничтожить другие данные и вызвать самые разнообразные ошибки. 
Использование нескольких префиксов
Команды обработки строк с несколькими префиксами работают ненадежно, и их следует по возможности избегать. 
  Необязательные операнды в командах обработки строк
Необязательные операнды в командах обработки строк используются только для задания размера данных и изменения сегмента и не гарантируют фактический доступ к данной области памяти. 
Уничтожение содержимого регистра при умножении
Умножение (8 на 8 бит, 16 на 16 бит, либо 32 на 32 бита) всегда уничтожает содержимое как минимум одного регистра, не являющегося накапливающим регистром, который используется в качестве исходного операнда. 
Ошибки, связанные с изменением содержимого регистров
Команды обработки строк, такие как MOVS, STOS, LODS, CMPS и SCAS, могут влиять на состояние некоторых флагов и содержимое трех регистров при выполнении единственной команды. При использовании команд обработки строк следует помнить, что содержимое одного 
из регистров SI или DI (или обоих сразу) получает положительное или отрицательное приращение (в зависимости от состояния флага направления) при каждом выполнении команды обработки строк. Содержимое регистра CX также получает отрицательное приращение как 
минимум один раз и, возможно, уменьшается до нуля при каждом использовании команды обработки строк с префиксом REP. 
Изменение состояния флага переноса
В то время как одни команды неожиданно для программиста влияют на состояние регистров и флагов, другие команды не влияют даже на те флаги, состояние которых было бы желательно изменить. 
Программист долго не использует флаги
Состояние флагов сохраняется до тех пор, пока не будет выполнена следующая команда, которая его изменяет, что обычно происходит достаточно быстро. Поэтому рекомендуется после установки флагов выполнять действия над ними как можно быстрее, чтобы избежать 
самых разнообразных ошибок, связанных с неверной установкой флагов. 
Смешение операндов в памяти и промежуточных операндов
Программа на языке Ассемблера может обращаться либо к смещению области памяти, в которой хранится переменная, либо к значению этой переменной. К сожалению, в языке Ассемблера нет ни интуитивных, ни строгих способов, позволяющих различить эти два вида обр
ащений, и в результате программисты часто путают обращения к смещению и обращения к значению. 
    Сохранение содержимого регистров при обработке прерываний
Каждый обработчик прерываний должен обязательно сохранять содержимое всех регистров. Хотя и допускается сохранять содержимое только тех регистров, которое изменяется данным обработчиком прерываний, для надежности работы все же рекомендуется заносить соде
ржимое всех регистров в стек при входе в обработчик прерываний и извлекать его из стека при выходе. 
Игнорирование групп в таблицах операндов и данных
Использование сегментных групп позволяет программисту логически разбивать данные на несколько областей, исключая при этом необходимость загружать сегментный регистр каждый раз, когда необходимо перейти от одной из таких логических областей данных к друго
й. 
Проверка
Создание программы с допустимыми входными данными составляет только часть функций проверки. В следующих разделах обсуждаются некоторые важные случаи проверки, которым должны подвергаться каждая программа, прежде чем можно будет сделать вывод о ее правиль
ной работе. 
Проверка граничных условий и случаи ограничения
Если вы считаете, что подпрограмма должна работать с данными, принимающими значение в определенном диапазоне, вы должны подвергнуть эту подпрограмму проверке с данными, принимающим различные значение в этом диапазоне. Например, если в вас имеется подпрог
рамма, выводящая на экран список длиной от 1 до 20 элементов, вы должны убедиться, что она ведет себя правильно и в том случае, когда в списке имеется ровно 1 элемент, и в том случае, когда в списке 20 элементов (здесь могут скрываться различные ошибки, 
в частности, ошибка типа "столбы и забор", описанная ранее). 
Ввод ошибочных данных
Когда вы убедитесь, что программа работает во всем диапазоне допустимых данных, следует убедиться, что она ведет себя корректно, когда вы задаете недопустимые входные данные. Например, убедившись, что предыдущая программа воспринимает значения в диапазон
е от 1 до 20, нужно также убедиться, что 0 или 21 значение ей отвергаются. 
Отсутствие входных данных
Этот момент при проверке и создании программы часто упускают. Если вы пишете программу, которая правильно себя ведет при отсутствии входных данных, работа с ней значительно упростится. 
Отладка, как часть процесса создания программы
Когда вы начинаете разработку программы, можно заранее запланировать этап отладки. Одно из основных компромиссных соглашений, которые необходимо установить при создании программы, заключается в том, в какой степени различные части вашей программы должны 
выполнять проверку на допустимые входные и выходные данные. 
При большом объеме проверок вы получите в результате очень гибкую программу, которая часто будет сообщать вам об ошибочной ситуации, но продолжать работать после выполнения некоторых действий по восстановлению. Однако при этом объем программы возрастет и
 работать она будет медленнее. Такой тип программ довольно легко отлаживать, поскольку до возникновения опасной ситуации подпрограммы сами сообщают вам о недопустимых входных данных. 
Можно также реализовать программу, в которой выполняется мало проверок на допустимость входных и выходных данных или такие проверки совсем отсутствуют. Такая программа будет меньшей по объему и будет быстрее выполняться, но неверные входные данные или ма
ленькая ошибка могут привести к аварийному завершению ее работы. Такой тип программ обычно труднее всего отлаживать, так как небольшая ошибка может проявиться при выполнении намного позднее. Это затрудняет выявление того места, где содержится ошибка. 
Большинство создаваемых программ сочетают в себе оба этих метода. Данные, воспринимаемые из внешних источников (например, вводимые пользователем или считываемые из файла на диске) подвергаются обычно более тщательной проверке, чем данные, передаваемые пр
и вызове от одной подпрограммы к другой. 
Пример сеанса отладки
В примере сеанса отладки используются те методы, о которых мы рассказывали в предыдущих разделах. Отлаживаемая программа TDDEMOB представляет собой вариант демонстрационной программы, испоьзованной в Главе 3 (TDDEMO.PAS), только в нее преднамеренно внесе
ны некоторые ошибки. Как и TDDEMO, TDDEMOB использует для вывода через Windows модуль WinCrt. 
Убедитесь, что в вашем текущем каталоге содержатся два файла, необходимые для демонстрации отладки. Вам понадобятся файлы TPDEMOB.PAS и TPDEMOB.EXE. (Буква B в именах файлов, означает, что в эту версию внесена ошибка.) 
Поиск ошибок
До того, как начать сеанс отладки, давайте запустим демонстрационную программу с ошибкой и посмотрим, что она делает неправильно. Программа уже скомпилирована на дистрибутивном диске. 
Чтобы просто посматривать исходный код, выполнять и отлаживать программу, запустите Турбо Паскаль для Windows (выбрав пиктограмму в менеджере программ Windows) и используйте для загрузки программы TDDEMOB команду FileіOpen (ФайліОткрытие). 
Когда вы увидите исходный код программы, перейдите в окно редактирования и с помощи команды RunіRun (ВыполнениеіВыполнение) или клавиш Ctrl-F9 запустите программу. 
Вам выведется подсказка для ввода строк текста. Введите две строки текста: 
ABC DEF GHI
abc def ghi
Последняя пустая строка и нажатите клавиши Enter завершает ваш ввод. После этого программа TDDEMOB выводит результаты анализа введенных вами строк: 
 9 letter(s) in 3 words in 2 lines(s)                         (1)
 Average of 0.67 words per line                               (2)
 Word length:   1  2  3  4  5  6  7  8  9  10                 (3)
 Frequency:     0  0  3  0  0  0  0  0  0  0                  (4)
 Letter:        M                                             (5)
 Frequency:     1  1  1  1  1  1  1  1  1  1  1  0  0  0  0   (6)
 Word starts:   1  0  0  1  0  0  1  0  0  0  0  0  0  0  0   (7)
 Letter:        Z
 Frequency:     0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
 Word starts:   0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
1 - 9 букв в 3 словах; 2 - в среднем 0.67 слов на строке; 3 - длина слова; 4 - частота; 5 - буква; 6 - частота; 7 - начинает слово 
В этой выходной информации содержится пять различных ошибок: 
   1. Число слов сообщается неверно (3 вместо 6).
   2. Число слов на строку неверно (0.67 вместо 3).
   3. В заголовках второй и третьей таблиц выводится только по одной букве (вместо A..M, N..Z). 
   4. Вы ввели две строки, каждая из которых содержит буквы от A до I, но в таблицах частоты букв показан только счетчик со значениеи 1 для этих букв. 
Теперь вы можете закрыть окно и вернуться в Турбо Паскаль для Windows. 
Выбор стратегии поиска ошибок
Первая задача состоит в том, чтобы решить с какой из ошибок разбираться в первую очередь. Здесь можно предложить хорошее правило: начинайте с той ошибки, которая появилась первой. В данной программе, после того, как данные инициализируются процедурой Ini
t, ввод с клавиатуры считывается функцией GetLine, а затем обрабатывается процедурой ProcessLine, пока пользователь не введет пустую строку. ProcessLine просматривает каждую строку ввода и обновляет глобальные счетчики. После этого процедурой ShowResults
 выводятся результаты. 
Среднее число слов в строке вычисляется процедурой ShowResults на основе числа строк и слов. Так как значение счетчика неверно, очевидно стоит вглянуть на процедуру ProcessLine и посмотреть, как изменяется значение переменной NumWords (число слов). Даже 
если значение NumWords верно, число 0.67 слов на строку не имеет смысла. Тогда ошибка возможно содержится в вычислениях процедуры ShowResults, на что также стоит обратить внимание. 
Заголовки для всех таблиц выводятся в результате обращения к процедуре ShowResults. Перед отслеживанием второй и третьей ошибки следует подождать завершения работы основного цикла. Так как счетчики слов и букв содержат неверные значения, вероятно что-то 
упущено в процедуре ProcessLine (это относиться к первой и четвертой ошибке). 
Теперь, после того, как обдумали проблему и наметили план ее решения, пришло время непосредственно начать отладку. 
Запуск Турбо отладчика TDW
Для того, чтобы начать отладку нашего примера, сделайте текущим окно редактирования Турбо Паскаля с пррограммой TDDEMOB, затем выберите команду RunіDebugger (ВыполнениеіОтладчик). 
Турбо отладчик TDW загрузит версию демонстрационной программы, содержащую ошибку, и выведет начальный экран, меню и т.д. Если вы хотите выйти из сеанса отладки и вернуться в Турбо Паскаль, нажмите клавиши Alt-X (это можно сделать в любой момент). Если вы
 безнадежно "заблудились", можно в любое время перезагрузить демонстрационную программу, нажав клавиши Ctrl-F2, и начать сначала (при этом точки останова и выражения просмотра очищены не будут). 
Для отладки таких подпрограмм, как ProcessLine, можно предложить два подхода. Вы можете либо выполнять ее построчно (по шагам), убедившись, что она все делает правильно, либо остановить программу непосредственно после выполнения процедуры ProcessLine и п
осмотреть, верны ли результаты. Так как оба счетчика содержат неверные значения, лучше внимательно проанализировать процедуру ProcessLine и посмотреть, как обрабатываются символы. 
Перемещение по программе
Итак, вы собираетесь запустить программу и исследовать процедуру ProcessLine. Сделать это можно несколькими способами. Можно нажать четыре раза клавишу F8 (пропуска трассировки вызовов процедур и функций), затем нажать один раз F7 (для трассировки вызова
 ProcessLine). Можно переместить курсор на строку 231, нажать клавишу F4 (команда Go to Cursor - Выполнение до курсора), а затем нажать один раз F7 для того, чтобы начать выполнение процедуры ProcessLine (Трассировка вглубь). 
Можно привести и другие способы, однако используем следующий. Нажмите клавиши Alt-F9. При этом вам выведется подсказка (диалоговое окно) для ввода адреса кода, до которого вы хотите выполнить программу. Наберите ProcessLine и нажмите клавишу Enter. Прогр
амма будет выполнена до того места, когда управление получает процедура ProcessLine. Когда вам выведется подсказка для ввода строки, введите те же данные, что и раньше (то есть, ABC DEF GHI). 
После ввода первой строки TDW возвращает вас в окно Module (Модуль), в котором выводится первая строка ProcessLine. ProcessLine содержит несколько циклов. Во внешнем цикле просматривается вся строка. Внутри данного цикла имеется цикл для пропуска символо
в, отличных от букв, а второй цикл обрабатывает слова и буквы. Переместите курсор к циклу while на строке 159 и нажмите клавишу F4 (Выполнение до курсора). 
Данный цикл будет выполняеться, пока он не достигнет конца строки, или не будет найдена буква. Последнее условие проверяется с помощью вызова булевской функции IsLetter. Для трассировки IsLetter нажмите клавишу F7. IsLetter представляет собой вложенную ф
ункцию, которая воспринимает значение символа и возвращает значение True (истинное значение), если это буква, и значение False в противном случае. При поверхностном анализе оказывается, что она проверяет только прописные буквы (верхний регистр). А она до
лжна проверять символы в диапазоне 'A'...'Z' и 'a'...'z' или перед выполнением проверки преобразовывать символы в верхний регистр. 
Еще один ключ к поиску ошибки дает анализ обеих введенных строк. Вы ввели буквы верхнего и нижнего регистра от 'A' до 'I', но в общем итоге выведена только половина букв. Теперь вы уже знаете, почему. Поскольку вторая из первоначально использованных вами
 строк, abc def ghi, содержит только буквы в нижнем регистре, каждый символ обрабатывается как пробел и пропускается. При пропуске букв не работает счетчик слов и счетчик букв, что решает проблему ошибок 1 и 4. 
Давайте вернемся назад к строке, в которой вызывается IsLetter, с помощью еще одного метода перемещения: нажмите клавиши Alt-F8, по которым программа будет выполнена до последнего оператора процедуры или функции. 
Диалоговое окно Evaluate/Modify
Кстати, существует еще один прекрасный способ выявить неправильное поведение IsLetter. Нажав клавиши Alt-D E, выведите диалоговое окно Evaluate/Modify (Вычисление/Модификация) и введите следующее выражение: 
        IsLetter('a') = IsLetter('A')
И тот, и другой параметр (a и A) являются буквами, но результат вычисления False подтверждает, то они интерпретируются функцией IsLetter по-разному. (Окна вычисления и просмотра можно использовать для вычисления выражений, выполнения присваиваний, или, к
ак в данном случае, вызовов процедур и функций. Более подробно об этом рассказывается в Главе 6.) 
Чтобы избавиться от диалогового окна Evaluate/Modify, нажмите клавишу Esc. 
Проверка
Итак, две ошибки выявлены, остались три. Ошибку 2 гораздо проще найти, чем предыдущие. Нажмите Alt-F8 для вызода из ProcessLine, затем переместите курсор к строке 207 и нажмите клавишу F4, чтобы выполнить программу до этой позиции курсора. 
Программа TDDEMOB выведет вам подсказку для ввода строки. Наберите abc def ghi и нажмите Enter. В ответ на повторный вывод подсказки просто нажмите Enter. Теперь нажмите клавишу F7 для трассировки процедуры ShowResults. 
Вспомните, что вы хотите определить, почему среднее число слов в строке имеет некорректное значение. В первой строке ShowResults вычисляется число строк на слово, а не число слов на строку. Ясно, что этот порядок следует изменить на обратный. 
Поскольку вы уже находитесь в данном месте, можно убедиться, что NumLines (число строк) и NumWords (число слов) имеют те значения, которые вы ожидаете. NumLines должно быть равно 2 и, поскольку вы нашли ошибку в IsLetter, но не исправили ее, NumWords дол
жно быть равно 3. Переместите курсор к NumLines и нажмите AltF10 I для проверки значения переменной. Окно Inspector (Проверка) показывает, что значение NumLines действительно равно 2. Теперь вы можете проанализировать NumWords. Нажмите клавишу Esc, чтобы
 закрыть окно Inspector, затем переместите курсор дальше на NumWords и снова нажмите Alt-F10 I (можно использовать также сокращение - Ctrl-I). NumWords содержит ожидаемое некорректное значение 3, поэтому можно следовать дальше. 
Однако стоит ли торопиться? В этих вычислениях есть еще одна ошибка, отсутствующая в нашем списке. Перед выполнением деления значение второй переменной не проверяется на 0. Если вы запустите программу сначала и совсем не введете данные (нажав от ответ на
 подсказку Enter), то программа аварийно завершит работу (даже если вы поменяете местами делимое и делитель). 
Чтобы убедиться в этом, нажмите Esc, чтобы закрыть окно Inspector, затем нажмите клавиши Alt-R P, чтобы завершить текущий сеанс отладки и F9, чтобы запустить программу сначала. В ответ на подсказку программу TDDEMOB нажмите клавишу Enter. Программа завер
шит работу (вы узнаете об этом, поскольку заголовок окна изменился на "inactive" - неактивно, а курсор исчез). Когда вы закроете окно, выведется окно ошибки с ошибкой этапа выполнения. Для возврата в TDW нажмите Enter, а затем нажмите клавишу Esc, чтобы 
избавиться от сообщения "Program terminated" ("Программа завершила работу"). 
Теперь вы знаете, что нужно проверить, поэтому оператор следует изменить следующим образом: 
        if NumLines <> 0 then
           AvgWords := NumWords / NumLines
        else
           AvgWords := 0;
С ошибкой 2 покончено. Поскольку вы работаете с окном Inspector (Проверка), попробуйте использовать его для просмотра структуры данных. Переместите курсор выше к описаню LetterTable на строке 50. Поместите курсор на слово LetterTable и нажмите клавиши Al
t-F10 I. Вы увидите, что это массив записей длиной в 26 элементов. Для просмотра каждого элемента массива используйте клавиши перемещения курсора, а для углубления в элемент массива - клавишу Enter. Это очень мощный способ проверки структур данных, он бу
дет особенно удобен для последующего исследования связанного списка в процедуре HeapOnParms. 
Выражения просмотра
Теперь давайте исследуем ошибку 3 в процедуте ShowResults (в выводе заголовка таблиц). Поскольку вы уже завершили программу, исследуя ошибку деления на 0, подготовьте ее для другого сеанса, нажав клавиши Alt-R P (сброс программы). Затем нажмите Alt-F9, н
аберите showresults и нажмите Enter. После этого введите уже знакомые вам данные ABC DEF GHI и нажмите клавишу Enter. Наконец, наберите abc def ghi и дважды нажмите Enter. Теперь нужно остановить Турбо отладчик TWD на ShowResults. 
В ShowResults для вывода таблиц букв используется вложенная процедура ShowLetterInfo. Переметите курсор на строку 113, нажмите клавишу F4, затем F7 для перехода в ShowLetterInfo. 
Здесь имеется три цикла for. В первом цикле выводится заголовок таблицы, а во втором и третьем - значения частот. Используйте клавишу F7 для перехода в первый цикл на строке 70. Позиционируйте курсор на FromLet и ToLet и используйте клавиши Alt-F10 I для
 проверки их значений. Они выглядят верными (первое равно 'A', а второе - 'M'). Нажмите клавиши Alt-F5 для вывода экрана пользователя. Для возврата к окно Module (Модуль) используйте любую клавишу. 
При выполнении подобного цикла очень удобно использовать окно Watch (Просмотр). Позиционируйте курсор на ch и нажмите Ctrl-W. Теперь для выполнения цикла по шагам используйте клавишу F7. Как и ожидалось, мы переходим к оператору Write на строке 71. Однак
о, если вы посмотрите на окно Watch (Просмотр), то увидите, что значение ch уже равно 'M' (уже выполнен весь цикл!). После ключевого слова do имеется лишняя точка с запятой, поэтому данный цикл 13 раз выполняется вхолостую. Когда управление переходит к о
ператору Write на строке 71, то выводится текущее значение ch ('M'). Устранение лишней точки с запятой позволяет избавится от ошибки 3. 
Завершение
Это все. Со всеми известными ошибками в этой программе покончено. Возможно, при следующем выполении данного кода вы найдете другие ошибки. Вы можете исправить ошибки (для удобства они отмечены двумя звездочками (**), затем перекомпилировать программу, ли
бо запустить версию данной программы без ошибок - TDDEMO.PAS (о ней рассказывается в Главе 3). 


Яндекс цитирования