The Unofficial Newsletter of Delphi Users - by Robert Vivrette


'bout Exceptions and the Map file

By Eddy Vluggen - evluggen@home.nl

One of Delphi's great features is it's exception handling, being not only very flexible and adaptable, but also very convenient. A lot of programs written in Delphi utilize the global exception handler, to catch unexpected errors. The way of implementation varies a lot, from a crude "Unexpected Error" message up to logging.

One thing which I've always missed was the location of the exception. I want Delphi to tell me where my program screws up. There are various third party products which can do this for you, but we can also build it ourselves, so when were done, we'll get nicer messages like:

The first thing we need is to generate a map file of our project. To do this, open the menu-item 'Project', 'Options', and take a look at the tab labeled 'Linker'. Set the map file to detailed, and rebuild your project. Delphi will now create a map file for your project, named after the executable file. The map file is a list of units, procedure names and line-numbers, with their according pointers. When Delphi generates an exception, it also gives a pointer to where this exception occurred.

Our first step is to retrieve this pointer, as it is not in the parameter list of the global exception handler. To get the address, we use the function ExceptAddr, from the SysUtils unit. You can display this pointer in your exception handler like this:

procedure TFormMain.ApplicationEvents1Exception(Sender: TObject; E: Exception);
begin
  ShowMessage(IntToStr(DWORD(ExceptAddr)));
end;

Now, there's a difference between this address and the ones in the map-file, due to the way your program is loaded into memory. To convert it, we need to keep consideration with the image base and the code base. You can set both during design time in the project options. Now, the code base can be handled as a constant, and the image base can be retrieved using hInstance. So, to get the address of the exception, and make it correspond to and address in the map-file, we would code:

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;

We now know for which address to look for in the map-file, what's left is to explain Delphi how to do this. We'll need to open the map-file, read it, remember its contents, and when an exception occurs, look up an exception.

A map-file has three sections which are of interest to us; one for units, another for procedures and finally one for line numbers. Since we want to read the file only once, we need to keep this information in memory. This can be done in a variety of ways, but for convenience sake I used a TList with records. So, every interesting section gets a TList, and the information from the section will be put in records. First, we declare our lists, and the record types:

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;

We open the map-file, and load the sections of interest into the lists, for future reference. I'm not going into detail about this, since the article is about exception handling and not about file-parsing. File parsing is a subject on it's own, with more methods and approaches then can be covered here. Let's just take a look at the map-layout, and see what goes in which list:

The units are listed in the section 'Detailed map of segments', and a single line looks like:

0001:00000000 000050E4 C=CODE S=.text G=(none) M=System ACBP=A9

The both addresses give use a range, and 'M=' give us the unit's name. The procedures are listed in two sections, 'Publics by Name', and 'Publics by Value'. These only have a starting address, and a procedure name:

0001:0004372C TCustomApplicationEvents.DoIdle

The linenumbers are at the end of the map-file, and for each unit, there's a block of addresses with their corresponding linenumber:

Line numbers for uMain(uMain.pas) segment .text

35 0001:000442C8 36 0001:000442DE 40 0001:000442F4 41 0001:000442F9

The map-file should be processed as early as possible in your application, to ensure that you have your list of unitnames and procedure names ready when an exception occurs. One possibility would be the initialization section of your mainform, and freeing the lists in the finalization section. You can use a TApplicationEvents to access the exception handler, but then you'll miss all the exceptions on startup. In the initialization section, your Form object hasn't been created yet, so you'll have to make the exception handler a class procedure. Your standard mainform would then look something like this:

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
  // Get exception address, and convert to map address
  // Lookup unitname, procedurename and linenumber
  // Display or Log message
end;

initialization
  LoadAndParseMapFile;
  Application.OnException := TForm1.GlobalExceptionHandler;
finalization
  CleanUpMapFile;
end.

Now, we are almost there. To look up a unit name, you should check for each item in the Units list, whether the address is in the range as specified in the record. This is quite forward actually, one just iterates trough the list, and checks if the corrected exception address is between the first adress from the 'Detailed map of segments', and the second. If so, break the iteration, and return the string from that line. Since we had the map-file in memory, in a TList, we check every record:

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;

The same way you can implement the search algorithm for the procedure list, and the list of linenumbers. Since there's no range here, you go backwards trough the list, and when you notice that your address is smaller than the current item from the list, you break.

This makes life a bit easier, as your Global Exception Handler now has the capability of pointing where your program is aching:

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(
    'Exception occurred: ' + E.Message + CrLf + CrLf +
    ' in unit: ' + UnitName + CrLf +
    ' in procedure: ' + ProcedureName + CrLf +
    ' on line: ' + LineNumber);
end;

To finish all up, I've included all procedures in a single unit, along with a small example project.

Note: the sample sources were compiled using Delphi 5, and also work with Delphi 6. When pressing Button1, you'll get an exception. If you press <F9>, you'll see the exception handler take care of it!