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>, то увидите , что заголовок прерывания помогает вам!

Возврат в Tips&Tricks