| The Unofficial
Newsletter of Delphi Users - by Robert
Vivrette О прерываниях и Map-файле By Eddy Vluggen - evluggen@home.nl Перевод Руденко Е.В. janer@newmail.ru декабрь 2001 года |
Одним из огромных преимуществ Delphi является возможность перехвата прерываний, не только гибкая , но и очень удобная. Большинство программ в Delphi используют глобальное прерывание для того , чтобы "выловить" неожиданные ошибки.
Я хочу , чтобы Delphi показывала место , где остановилась моя программа. Есть различные компоненты, которые делают это, но мы можем сделать этот сами и получать вот такое красивое сообщение об ошибке:
Первое, что нам необходимо, это - создать map -файл нашего проекта. Чтобы сделать это, откроем меню 'Project', 'Options', 'Linker'. Установите галочку для создания map- файла и перекомпилируйте ваш проект. Delphi создаст map- файл для вашего проекта с именем запускаемого файла. Map-файл - это лист модулей , имен процедур с номерами строк, с указателями на них. Когда Delphi генерирует прерывание, в этом файле появляется указатель на место появления прерывания.
Наш первый шаг заключается в восстановлении этого указателя, так как его нет в листе параметров глобального прерывания. Чтобы получить адрес , мы используем функцию ExceptAddr из модуля SysUtils.
Указатель можно показать следующим образом:
procedure TFormMain.ApplicationEvents1Exception(Sender: TObject;
E: Exception);
begin
ShowMessage(IntToStr(DWORD(ExceptAddr)));
end;
Отметим, что есть отличие между этим адресом и адресом в map-файле из-за того, как ваша программа загружается в память. Для преобразования нам необходимо понять разницу между image base и code base. Обе опции вы можете установить на стадии разработки вашего проекта. Таким образом, code base можно хранить как константу , а image base - используя параметр hInstance. Для выполнения напишем следующий код:
function GetMapAddressFromAddress(const Address: DWORD):
DWORD;
const
CodeBase = $1000;
begin
Result := Address - (hInstance + CodeBase);
end;
procedure TFormMain.ApplicationEvents1Exception(Sender: TObject; E:
Exception);
var
MapFileAddress: DWORD;
begin
MapFileAddress := GetMapAddressFromAddress(DWORD(ExceptAddr));
ShowMessage(IntToStr(MapFileAddress));
end;
Теперь мы знаем, что соответствует каким адресам в map-файле. Нам необходимо открыть map-файл, прочитать его, запомнить его содержание и при возникновении прерывания просмотреть его.
Map-файл имеет три секции, которые интересны нам; одна для модулей, другая для процедур и последняя для номеров строк. Поскольку нам необходимо прочитывать файл неоднократно, то необходимо сохранить информацию в памяти. Это можно сделать различными способами, но для удобства я использую компонент TList с записями. Итак, для каждой секции создается TList, и информация из секции размещается в записи. Сначала мы продекларируем наши листы и типы записей:
var
Units,
Procedures,
LineNumbers: TList;
type
TUnitItem = record
UnitName: string;
UnitStart,
UnitEnd: DWORD;
end;
TProcedureItem = record
ProcName: string;
ProcStart: DWORD;
end;
TLineNumberItem = record
UnitName,
LineNo: string;
LineStart: DWORD;
end;
Модули перечислены в секции 'Detailed map of segments' и типичная строка имеет вид :
0001:00000000 000050E4 C=CODE S=.text G=(none) M=System ACBP=A9
Вначале представлен диапазон адресов , а после символа 'M=' написано имя модуля. Процедуры размещены в двух секциях 'Publics by Name', и 'Publics by Value'. Здесь строки состоят только из начального адреса и имени процедуры:
0001:0004372C TCustomApplicationEvents.DoIdle
Номера строк размещены в конце map-файла и для каждого модуля представлен блок адресов с соответствующими номерами строк:
Line numbers for uMain(uMain.pas) segment .text
35 0001:000442C8 36 0001:000442DE 40 0001:000442F4 41 0001:000442F9
Map-файл должен создаваться как можно раньше в вашем приложении, чтобы гарантировать создание списка модулей и имен процедур до возникновения прерывания. Годится например секция инициализации (initialization) вашей главной формы . Вы можете использовать событие TApplicationEvents для доступа к заголовку (handler) прерывания, но в этом случае будут пропущены все прерывания при старте приложения. В секции инициализации объект вашей Формы еще не создается полностью, так что вам необходимо добавить class procedure для заголовка прерывания. В этом случае код для вашей главной формы будет иметь следующий вид:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs;
type
TForm1 = class(TForm)
private
{ Private declarations }
public
{ Public declarations }
class procedure GlobalExceptionHandler(Sender:
TObject; E: Exception);
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
class procedure TForm1.GlobalExceptionHandler(Sender: TObject; E:
Exception);
begin
// Получить адрес прерывания и
конвертировать в адрес Map-файла
// Просмотр имен модулей, процедур и
номеров строк
// Отображение на экране или Log -
сообщение
end;
initialization
LoadAndParseMapFile;
Application.OnException := TForm1.GlobalExceptionHandler;
finalization
CleanUpMapFile;
end.
Это почти все. Просмотрев имена модулей, можно найти запись с адресом , совпадающим с адресом прерывания. Для этого достаточно только раз пройти весь список и отметить , находится ли адрес прерывания между первым адресом из 'Detailed map of segments', и вторым. Если это так, то прервите просмотр и просмотрите строку данной линии. Поскольку map-файл хранится в памяти в TList, мы можем проверить каждую строку:
function GetModuleNameFromAddress(const Address: DWORD): string;
var
i: Integer;
begin
for i := Units.Count -1 downto 0 do
if ((UnitItem(Units.Items[i]).UnitStart <=
Address) and
(UnitItem(Units.Items[i]).UnitEnd >=
Address)) then
begin
Result := UnitItem(Units.Items[i]).UnitName;
Break;
end;
end;
Таким же образом вы можете повторить алгоритм поиска для списка процедур.
Это делает жизнь немного легче, так как Заголовок глобального прерывания имеет теперь возможность указывать на слабое место вашей программы:
procedure TFormMain.ApplicationEvents1Exception(Sender: TObject; E:
Exception);
var
MapFileAddress: DWORD;
UnitName,
ProcedureName,
LineNumber: string;
const
CrLf = #10#13;
begin
MapFileAddress := GetMapAddressFromAddress(DWORD(ExceptAddr));
UnitName := GetModuleNameFromAddress(MapFileAddress);
ProcedureName := GetProcedureNameFromAddress(MapFileAddress);
LineNumber := GetLineNumberFromAddress(MapFileAddress);
ShowMessage(
'Прерывание наблюдается: ' + E.Message + CrLf + CrLf +
' в модуле: ' + UnitName + CrLf +
' в процедуре: ' + ProcedureName + CrLf +
' в строке: ' + LineNumber);
end;
Чтобы закончить , я включил все процедуры в небольшой пример.
Примечание: пример был скомпилирован с Delphi 5 , но работает и с Delphi 6. При нажатии кнопки Button1, вы получаете прерывание. Если вы нажмете кнопку <F9>, то увидите , что заголовок прерывания помогает вам!