Как выполняются Windows-программы
Отладочные команды
Стратегия отладки Windows-программ
Установка точек прерывания
Отображение трассы выполнения
Окончательное тестирование
Некоторые рекомендации

Занятие третье

Продолжение. Начало см. в # 10, 11/97.

Этим занятием мы завершаем наш учебный курс для пользователей системы Borland C++ Builder. Темой этого занятия станет отладка приложений. Мы рассмотрим технологию отладки событийно-управляемых приложений и возможности интегрированного отладчика C++ Builder.


Как выполняются Windows-программы

Чем отличается выполнение DOS-приложений от выполнения приложений, управляемых событиями, каковыми являются все приложения для Windows? Непредсказуемостью! Да-да, именно непредсказуемостью. В DOS ваша программа выполнялась от инструкции к инструкции, периодически обращаясь за помощью к операционной системе: где буковку на дисплей вывести, где клавишу считать, где прерывание какое-нибудь вызвать. В Windows все иначе: ваше приложение запускает себя, создает окошко (а может быть, и не одно) и отдает себя в руки Windows, которая становится главным распорядителем. С этого момента все команды, связанные с действиями пользователя, обрабатывает операционная система, и уже она решает, отдать или нет управление вашей программе. Это и является причиной непредсказуемости, называемой системными программистами асинхронностью. Ваше приложение просто не может знать, когда его "позовут к столу" и по какому поводу. Возможно, пользователь выберет пункт из меню, а может быть, сработает системный таймер или передвинется мышь, кто ж его знает... Если событие все-таки возникает, Windows ищет в вашей программе соответствующий обработчик этого события (про обработчики мы уже с вами говорили на предыдущем занятии). Если таковой имеется, то управление передается ему. Последний делает свое дело и возвращает управление Windows. Если управление не вернуть, то в окне менеджера задач напротив названия вашей программы возникнет строчка Not responding ("Не отвечает"). Ну а дальше вы сами знаете, что с такими программами нужно делать.

Отладочные команды

Познакомимся с командами для отладки из меню Run. Их довольно много, хотя в режиме компиляции и редактирования многие отключены. Перечислим интересующие нас пункты меню и их назначение:

Run - запустить приложение на выполнение;
Parameters - задать параметры командной строки для отлаживаемой программы;
Step Over - пошаговое выполнение программы без захода внутрь вызываемых функций;
Trace Into - пошаговое выполнение программы с заходом внутрь вызываемых функций;
Trace To Next Source Line - выполнить шаг к следующей исполняемой строке исходного текста;
Run To Cursor - выполнить программу до участка, где расположен курсор;
Show Execution Point - установить курсор на участок выполнения;
Program Pause - временно приостановить выполнение отлаживаемой программы;
Program Reset - остановить выполнение программы, сбросить контекст выполнения задачи и освободить память;
Evaluate/Modify - открыть диалоговую панель для вычисления выражений и модификации переменных и результатов вычисления выражений;
Inspect - открыть диалоговую панель инспектора, где можно просмотреть значения переменных и выражений;
Add Watch - открыть диалоговую панель просмотра данных и добавить в нее новые данные;
Add Breakpoint - открыть диалоговую панель для установки и изменения точек прерывания.

Стратегия отладки Windows-программ

Отлаживая программу, программист обычно пользуется несколькими стандартными операциями:

  • установка точек прерывания (breakpoints);
  • просмотр содержимого переменных и участков памяти;
  • подстановка ложных данных для моделирования ситуации "что - если";
  • ложные вызовы.
  • Эти шаги одинаковы для отладки любых программ, выполняющихся на любых платформах. Однако техника их применения может несколько отличаться. Так, например, прогнозировать выполнение программы в DOS легко, а выполнение приложения в Windows может быть лишь смоделировано. Из этого следует, что при отладке DOS-программу можно выполнять пошагово, перебираясь от одной строки исходного текста к другой. В Windows это нереально. Нужно поставить точку прерывания на интересующем вас участке программы и запустить ее на выполнение командой Run - Run. Достигнув точки прерывания, программа приостановит свое выполнение, давая возможность программисту приступить к отладочным действиям.

    Установка точек прерывания

    В приложениях, выполненных с применением C++ Builder, точки прерывания удобнее всего ставить внутри обработчиков событий. В файле проекта это делать бесполезно, потому что он сгенерирован автоматически, и вряд ли вам придется вносить туда изменения, что способно вызвать ошибки.

    Простейший способ установить точку прерывания - нажать на клавишу . При этом выбранная для остановки строка выделяется красной полосой, на левом краю строчки появляется маленький значок "Stop".

    Повторное нажатие на отменяет уже имеющуюся точку прерывания. Другой способ установить точку прерывания - щелкнуть мышью на левом краю окна редактирования. Результат будет таким же. Ну и наконец можно вызвать команду меню Run - Add Breakpoint. Появится диалоговая панель редактирования точек прерывания Edit Breakpoint с несколькими полями, среди которых не только координаты файла и номера строки (где будет задана точка прерывания), но и некоторые другие полезные данные. К примеру, вы можете задать параметр Condition, где можно ввести выражение, при истинности которого точка прерывания сработает. Как показано на рис. 2, в строке 13 точка прерывания настроена так, чтобы сработать лишь в том случае, когда переменная Application не равна нулю. Иначе выполнение программы не будет прервано при прохождении через эту строку.

    Дополнительно можно задать количество проходов, после которых точка прерывания переходит в активное состояние. Это весьма удобно при отладке длинных циклов, когда необходимо пропустить некоторое количество итераций.

    Как правильно устанавливать точки прерывания? Это довольно сложный вопрос. Наверняка узнать правильное местоположение трудно, но есть два простых правила, которые помогут вам при отладке.

    Попытайтесь определить место потенциальной ошибки. Это не всегда можно сделать, но попытаться надо. К примеру, вы нажимаете кнопку, а в ответ ничего не происходит или происходит не то, что вы ожидали. Ясно, что ошибка возникает либо в обработчике нажатия кнопки, либо в методах и функциях, которые из него вызываются. Отсюда следует, что установить точку прерывания нужно в начале данного обработчика.

    Если не удается определить место потенциальной ошибки даже примерно, нужно поставить точки прерывания в начале каждого обработчика или, если их много, в группе из нескольких обработчиков. Затем воспроизведите ошибочную ситуацию и найдите, какая из точек прерывания сработает. Остальные можно снять за ненадобностью.

    Когда ошибочный обработчик найден, нужно приступать к уточнению места ошибки. Для этого подумайте, что должно происходить в каждой строке программы в соответствии с логикой ее работы, и начинайте продвигаться шаг за шагом командой Trace Into (можно просто нажимать клавишу ) или Step Over (клавиша ), проверяя, как изменяются данные вашей программы. Можно поступить и по-другому, поставив точку прерывания где-нибудь дальше в исходном тексте и вызвав команду Run (клавиша ). После остановки на точке прерывания проверяются данные вашей программы. Если они изменились не так, как это предполагалось, то, очевидно, ошибка находится на только что пройденном участке. Если же все в порядке, то идем дальше. При таком методе поиска нужно помнить, что если на пути встречаются операторы ветвления или перехода, то выполнение программы может пойти по непредсказуемому маршруту. Так что лучше все ветвления и переходы трассировать вручную командами Step Over и Trace Into.

    Ошибки в программе могут быть самыми разнообразными. Но для Windows-приложений характерны следующие потенциальные "жучки":

  • вызов неверной функции, когда вместо одной функции по ошибке вызывается другая;
  • вызовам Windows API передаются неправильные аргументы, вследствие чего Windows игнорирует вызов функции API или возвращает ложный результат (выполняет не то действие, которое ожидалось);
  • задание неверных параметров цикла, когда цикл выполняется не то количество раз, которое предусмотрено разработчиком вследствие неверной инициализации или граничных параметров;
  • ошибка в условии операторов if, switch, while и т.д., приводящая к неправильному ветвлению;
  • возникновение не предусмотренного программистом варианта реакции системы.
  • Отображение трассы выполнения

    Во многих случаях обнаружить ошибку поможет трассировка содержимого переменных с помощью функции API OutputDebugString(), которая пересылает строку-аргумент в отладчик. В Borland C++ Builder все такие строки отображаются в отдельном окне редактора как содержимое файла OutDbgX.txt, где X - некий уникальный номер. Проанализируйте полученный поток сообщений и найдите ошибку, произошедшую по ходу выполнения. Можно выводить с помощью OutputDebugString() и значения переменных. Нужно только преобразовать их в строку. Ниже приводится фрагмент файла проекта с трассировочными сообщениями:

    ...
            try
            {
                    Application->Initialize();
                            OutputDebugString("Initialized...");
                    Application->CreateForm(__classid(TForm1), &Form1);
                            OutputDebugString(
                                    AnsiString( "Main form created " +
                                    IntToHex( int(Form1), 8) ).c_str() );
                    Application->Run();
                            OutputDebugString("Finishing...");
            }
    ...

    При прохождении каждого нового участка исходного текста отладчику посылается строка, говорящая о состоянии программы. А после создания главной формы выводится указатель на ее экземпляр. Выглядит это сложновато, потому что нужно привести указатель к типу int, чтобы затем его преобразовать в строку шестнадцатеричного формата. Затем полученная строка типа AnsiString преобразуется вызовом метода c_str() к обычной цепочке байтов с нулем на конце. Можно упростить задачу и использовать функции sprintf wsprintf библиотеки языка Cи++.

    Окончательное тестирование

    Предположим, все ошибки найдены и устранены. Но червь сомнения все равно не дает покоя: а не осталось ли чего еще... Это значит, что вы подошли к этапу окончательного тестирования и вам предстоит изрядно повозиться. Первейшее средство для выявления скрытых ошибок - утилита Stress из комплекта разработчика Microsoft SDK. Запустите через нее свою программу, задав "нечеловеческие" условия работы вроде нехватки оперативной памяти, подкачки на диск и т. д. Очень вероятно, что многие огрехи программирования мгновенно выплывут. Если же и в этом случае все в порядке, то следует поискать другой род ошибок: ошибки логические. На их поиске обычно седеют даже опытные разработчики. Воспользуйтесь методом крайних элементов: отдельно взятую функцию программы вызывайте с различными аргументами. Сначала это нормальные допустимые логикой аргументы, затем минимальные аргументы, потом максимальные и, наконец, аргументы, выходящие за минимальный и максимальный допустимые пределы. Получаемый результат сравните с ожидаемым. Если все нормально, функцию можно считать благополучной и переходить к следующей. Для экономии времени лучше всего начинать с функций верхнего уровня. Тогда при благоприятном исходе можно отсечь все подчиненные функции более низкого уровня.

    Для удобства работы не поленитесь и напишите маленькую тестовую программу, которая сделает все необходимые вызовы функции с различными аргументами за один проход. Результат выведите с помощью функции OutputDebugString(). Такую программу достаточно будет запустить один раз и просмотреть полученную трассу выполнения. Если же этот метод для вас неприемлем, то можно воспользоваться командой Run - Evaluate/ Modify. В появившейся диалоговой панели аргументы можно задавать вручную по ходу выполнения программы. В примере, показанном на рис. 3, потребовалось узнать, что произойдет, если изменить значение переменной Editor->SelStart. Открыв диалоговую панель Evaluate/Modify, мы увидели, что первоначальное значение интересующей нас переменной равно нулю. Затем мы ввели значение 100 в поле New Value и нажали на кнопку Modify. Значение Editor->SelStart поменялось с 0 на 100. Это легко проверить командой Inspect (или нажав + ). Кстати, существует маленькая хитрость, позволяющая экономить время. Вместо того чтобы вводить название проверяемой переменной, вы можете установить на нее курсор или выделить ее как текст, а уже после этого вызывать команды Run - Evaluate/ Modify или Run - Inspect. С++ Builder сам догадается, какую переменную вы собираетесь просмотреть, и предоставит вам информацию о ней.

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

    Некоторые рекомендации

    Эти маленькие советы касаются выбора ракурса просмотра данных или просто помогут вам сэкономить время:

  • если вы собираетесь посмотреть содержимое переменной или класса, воспользуйтесь командой Inspect (+ );
  • если вы намерены наблюдать за изменением значений переменных по ходу выполнения программы, то лучше внести их в список Watch (+), а окно Watch разместить в углу экрана;
  • все эксперименты по изменению значений и вычислений лучше проводить в окне Evaluate/ Modify;
  • просматривать последовательные данные вроде блоков памяти или массивов лучше всего в окне Watch, задав при этом размер цепочки данных и стиль просмотра;
  • чтобы остановить выполнение программы на ходу, нажмите +, но старайтесь не злоупотреблять этой возможностью, так как она рано или поздно приводит к сбою Windows.
  • И последнее: никогда не поздно попробовать запустить откомпилированное приложение без отладчика. Бывает, что вне последнего программа работает нормально.