Под
силовой отладкой (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, представленным
на рисунке, мне стоит всерьез подумать о наращивании памяти своего компьютера.
Рис 2.23 Использование функции ShowMessage для вывода отладочной информации.
Вывод
на консоль
Еще один способ вывода отладочной информации— вывод на
консоль с использованием процедур 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;
Результат показан на рис. 2.24.
Опытные пользователи 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.
Как правильно использовать файл протокола? Какую информацию в него записывать? Сколько программистов, столько и ответов на эти вопросы.