Под силовой отладкой
(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.
Как правильно использовать файл протокола? Какую информацию в него записывать? Сколько программистов, столько и ответов на эти вопросы. Лично я предпочитаю придерживаться золотой середины между "записывай все" и "записывай только то, что отлаживаешь".