Delphi &         Pascal

Все что необходимо...

Силовая отладка

   

    Под силовой отладкой (brute-force debugging), отладкой "в лоб", понимаются методы отладки, основанные не на возможностях отладчиков, а на трюках, родословная которых, пожалуй, восходит к временам Атанасова и Лебедева, создававших первые ЭВМ по обе стороны океана.
    При разработке программ часто нет необходимости в полной отладке, просто хочется убедиться в том, что какая-либо функция работает так, а не иначе (я весьма часто попадаю в подобные ситуации, когда использую малознакомые функции API или плохо или вовсе недокументированные методы объектов, и мне надо провести эксперимент, чтобы выяснить, так ли я представляю работу функции).
    В этих случаях проще забыть об отладчике и просто добавить пару строк кода для вывода информации. Для этого есть много путей, и о некоторых из них будет рассказано ниже.


ПРЕДОСТЕРЕЖЕНИЕ: Большинство таких методов — из серии "быстро и грязно", и я бы не рекомендовал заменять ими описанные ранее методы тестирования и отладки.

Вывод отладочной информации в форме.
    Один из способов вывода такой информации — ее вывод непосредственно в форме. Обычно проще всего создать компонент TLabel или подобный ему для непосредственного вывода информации. В таком случае выведенная информация не потеряется даже при перерисовке формы.
    Посмотрите на описания функций ExtractFileDir и ExtractFilePath в справочной системе Delphi 4. Я не берусь точно судить по документации о различии между этими функциями, но я знаю, что мне делать. Я создаю новое приложение (выбрав пункт меню File/New Application) и помещаю в главную форму элемент TButton и два элемента TLabel (форма будет выглядеть так, как на рис. 2.20).
    Дважды щелкните на кнопке TButton и добавьте код к обработчику события OnClick.

    procedure TFormI.ButtonlClick(Sender: TObject);
    begin
        Labell.Caption:= ExtractFileDir(Application.ExeName);
        Label2.Caption:= ExtractFilePath(Application.ExeName);
    end;

    (Application. ExeName возвращает полное имя файла приложения). Нажмите клавишу <F9> для компиляции и запуска приложения и щелкните на кнопке. Теперь вам должно быть ясно, чем различаются эти две функции.
Недавно у меня возникла проблема с чужой DLL, исходного кода которой я, естественно, не имел. Странным было то, что эта DLL выцелела при загрузке и не освобождала большой фрагмент виртуальной памяти. Я создал маленькое приложение, в котором после каждого щелчка на кнопке сообщалось, сколько виртуальной памяти свободно. Мне хотелось сохранять предыдущие результаты, а потому, я использовал элемент управления TMemo и добавлял в него новые строки с результатами.
     Чтобы посмотреть, как это делается, создадим новое приложение и разместим в форме элементы управления TMemo и TButton (и не забудем установить значение свойства TMemo.ScrollBars равным ssVertical). Ваша форма будет выглядеть так, как на рис. 2.21.

 
 

    В обработчик события OnClick добавьте следующий код.

    procedure TFormI.ButtonlClick(Sender: TObject);
    var
        MemStat: TMemoryStatus;
    begin
        VirtualAlloc(nil, 1000000, MEM_RESERVE, PAGE_READWRITE);// 1
        MemStat.dwLength:= SizeOf(TMemoryStatus);              // 2
        GlobalMemoryStatus(MemStat);                            // 3
        Memol.Lines.Add(IntToStr(MemStat.dwAvailVirtual));      // 4
    end;

Не беспокойтесь о деталях вызова API-функции VirtualAlloc в строке 1. Здесь ее вызов требует от операционной системы зарезервировать миллион байтов памяти для дальнейшего использования. API-функция GlobalMemoryStatus возвращает информацию об использовании памяти приложением и системой в целом. Информация возвращается в переменной MemStat, представляющей собой запись типа TMemoryStatus. Перед вызовом GlobalMemoryStatus вы передаете системе информацию о размере структуры, как в строке 2, а затем вызываете функцию (строка 3) и выводите информацию в TMemo в строке 4.
    Скомпилируйте и запустите программу, щелкните несколько раз на кнопке - и увидите, что виртуальная память уменьшается примерно на один мегабайт при каждом щелчке, как и ожидалось. На рис. 2.23 показана форма после нескольких щелчков на кнопке.
    Используя этот метод (без вызова VirtualAlloc), я выяснил, что на самом деле DLL затребовала около 60 Мбайт (!) виртуальной памяти при загрузке и не освободила ее. Даже притом, что Windows 95 предоставляет каждому приложению двухгигабайтовое адресное пространство, потерю 60 Мбайт сложно проигнорировать...
 

ShowMessage
    Кроме вывода информации в форму, можно воспользоваться модальным диалоговым окном. Принципиальное отличие этого метода, в первую очередь, состоит в том, что модальное диалоговое окно останавливает выполнение программы, пока вы его не закроете. Таким образом, у вас имеется достаточно времени, чтобы прочесть и осмыслить полученную информацию.
    Процедура ShowMessage (из модуля Dialogs) идеально подходит для этой цели Она позволяет вывести строку любой длины в простом модальном диалоговом окне. Вам только следует создать строку для вывода и передать ее процедуре (можно также использовать MessageDIg, но в нем слишком много шашечек и бантиков, которые требуют немалых усилий для достижения того же эффекта).
    ShowMessage получает в качестве параметра одну строку, для создания которой я предпочитаю использовать функцию Format, она идеально подходит для этого, будучи одновременно простым и мощным инструментом в умелых руках.
    Рассмотрим простой пример. Используем этот метод для вывода информации, получаемой от уже использовавшейся функции GlobalMemoryStatus.
    Создадим новое приложение и поместим TButton в основную форму. Обработчик события OnClick будет выглядеть следующим образом.

    procedure TFormI.ButtonlClick(Sender: TObject);
    var MemStat: TMemoryStatus;
    begin
        MemStat.dwLength:= SizeOf(TMemoryStatus);
        GlobalMemoryStatus(MemStat);
        with MemStat do ShowMessage(Format('Memory load: %d%%'#13 +
            'Total physical: %d'#13+'Available physical: %d'#13 +
            'Total page file: %d'#13 + 'Available page file: %d'ftl3 +
            'Total virtual: %d'#13 + 'Available virtual: %d',
            [dwMemoryLoad, dwTotalPhys, dwAvailPhys, dwTotalPageFile,
            dwAvailPageFile, dwTotalVirtual, dwAvailVirtual]));
    end;

    Заметьте, что я внес в строку несколько символов #13 (ASCII-символ возврата каретки). Это позволяет разбить строку при выводе на несколько строк, что существенно облегчает чтение информации. На рис 2.23 показано, что получится после запуска программы и щелчка на кнопке.
    Судя по результатам Memory load и Available physical, представленным на рисунке, мне стоит всерьез подумать о наращивании памяти своего компьютера.
 

Вывод на консоль
    Еще один способ вывода отладочной информации— вывод на консоль с использованием процедур Write и WriteLn. Вы можете конвертировать проект в консольное приложение, например, выбрав соответствующую опцию (команду Project/Options, вкладку Linker и опцию Generate Console Application) или поместив директиву $APPTYPE CONSOLE в главный DPR-файл. Учитывая, что ваше приложение— не консольное, воспользуйтесь возможностями условной компиляции и используйте директиву $APPTYPE как показано ниже:

    {$ifdef Debug}
    {$APPTYPE CONSOLE}
    {$endif}

    Теперь вывод на консоль будет осуществляться только в отладочной версии вашего приложения.
    Если вы попытались использовать функцию Write или WriteLn и получили сообщение об ошибке I/O Еггог,  значит, вы забыли сделать проект консольным приложением.
    Обратите внимание, что здесь применяется тот же код, что и раньше, но теперь мы используем вывод на консоль вместо ShowMessage. Убедитесь, что вы создаете консольное приложение, и измените обработчик так, как показано ниже.

    procedure TFormI.ButtonlClick(Sender: T0bject);
    var MemStat: TMemoryStatus;
    begin
        MemStat.dwLength:= SizeOf(TMemoryStatus);
        GlobalMemoryStatus(MemStat);
        with MemStat do
        begin
            WriteLn(Format('Memory load: %d%%',[dwMemoryLoad]));
            WriteLn(Format('Total physical: %d',[dwTotalPhys]));
            WriteLn(Format('Available physical: %d',[dwAvailPhys]));
            WriteLn(Format('Total page file: %d',[dwTotalPageFile]));
            WriteLn(Format('Available page file: %d',[dwAvailPageFile]));
            WriteLn(Format('Total virtual: %d',[dwTotalVirtual]));
            WriteLn(Format('Available virtual: %d',[dwAvailVirtual]));
        end;
    end;

    Опытные пользователи Pascal заметят, что функция Format использовалась там, где это не было необходимо (WriteLn имеет свои возможности форматирования). Однако я везде использую Format как мощный инструмент; кроме того, используя везде одну лишь функцию Format, я избавляюсь от необходимости помнить два набора правил форматирования.

Запись в Log-файл
    Запись отладочной информации в файл протокола (Log-файл) существенно отличается от предыдущих приемов записи, так как это уже нельзя назвать "быстро и грязно". Это отличная технология, которую можно использовать в любом приложении.
    Запись в файл протокола выполняется так же, как и вывод на консоль, но вместо WriteLn (. . . ) используется WriteLn (LogFile, . . . ), где LogFile — имя файловой переменной типа TextFile. Надо также не забывать открывать этот файл в начале работы приложения и закрывать — в конце. Проще всего этого добиться, поместив соответствующий код в свой модуль, который благодаря возможности условной компиляции подключается только в отладочной версии вашей программы.

Листинг 2.1. Модуль протоколирования отладочной информации.

    unit uLoq;
    interface
    procedure Log(S: Strings-implementation uses
        Windows, SysUtils;
    var
        LogFile: TextFile;
        LogCriticalSection: TRtlCriticalSection;
    procedure Log(S: String);
    var
        SystemTime: TSystemTime;
        FileTime: TFileTime;
    begin
        GetSystemTime (SystemTime) ;
        SystemTimeToFileTime(SystemTime, FileTime) ;
        EnterCriticalSection(LogCriticalSection);
        WriteLn(LogFile, Format('%s %.8x%.8x %5',
            [FormatDateTime('yy.mm.dd hh.inm.ss'. Now),
            FileTime.dwHighDateTime, FileTime.dwLowDateTime, S])) ;
        LeaveCriticalSection(LogCriticalSection) ;
    end;
    procedure Startup;
    var
        FileName: String;
    begin
        InitializeCriticalSection(LogCriticalSection);
        FileName := Format("Log file for %s at %s.txf,
            [ParamStr(O), DateTimeToStr(Now)]) ;
        while Pos(':', FileName) 0 do
            FileName[Pos(':', FileName)] := '.';
        while Pos('/', FileName) 0 do
            FileName[Pos('/', FileName)] := '-';
        while Pos('\', FileName) 0 do
            FileName[Pos('\', FileName)] := '.';
        AssignFile(LogFile, FileName);
        Rewrite(LogFile) ;
    end;
    procedure Shutdown;
    begin
        CloseFile(LogFile) ;
        DeleteCriticalSection(LogCriticalSection) ;
    end;
    initialization Startup;
    finalization Shutdown;
end.

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

    unit MyUnit;
    interface
    uses
        ($ifdef Debug} uLog, {$endif)
        Windows, Messages, SysUtils, Classes,
        . . .

    Затем используйте его приблизительно так.

    {$ifdef Debug)
    Log(Format('Entering the Foo procedure; Bar = %d',[Bar]));
    {$endif}

    He забывайте размещать вызов между директивами условной компиляции, иначе при компиляции коммерческой версии возникнет ошибка.
    Модуль uLog обладает двумя интересными и полезными свойствами. Во-первых, каждая запись в файл предваряется информацией о дате, времени и шестнадцатеричным числом, соответствующим системному времени в миллисекундах. Эта информация может быть весьма полезной, особенно когда вы хотите отследить последовательность событий в приложении. Во-вторых, модуль использует критические разделы (critical section), что обеспечивает доступ к файлу только одной подзадачи в один момент времени.
    На рис. 2.25 показан типичный файл протокола в программе Notepad.

 

    Как правильно использовать файл протокола? Какую информацию в него записывать? Сколько программистов, столько и ответов на эти вопросы. Лично я предпочитаю придерживаться золотой середины между "записывай все" и "записывай только то, что отлаживаешь".

Далее

Hosted by uCoz