Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
This article is part of a two-part Delphi/C++ collaboration. This Delphi part describes the design and implementation of a little debugging tool, while the C++ part describes the deployment of this tool in the Borland C++ IDE. I suggest you read both parts for the whole story.
When?
Whenever you write programs, you sometimes also make mistakes.
At least I do.
Some of these mistakes result in a runtime error of the program, which looks a bit like this (while the actual appearance may differ, the contents will essentially be the same):
What?
The system modal message you get shows you two important kinds of information.
First of all, the number of the run-time error.
Often, this will be 216, which is the same as a General Protection Fault.
A GPF is most often caused by writing to something that doesn't belong to you (like a memory or variable overwrite).
The runtime error above can be found in the manual or on-line help to be a stack error.
Where?
More important, however, is the place where this runtime error occurred.
The message gives us that information, too.
The error address ($1234:$5678 in the example above) can be used to get to the exact source file and line number where the error occurred.
Note that this will often be the place where the error was triggered, but not necessarily the cause of the error.
A memory overrun may be caused by an earlier memory allocation that was too short (or forgotten at all), for example.
The place to start looking for this address is the MAP file that belongs to the application.
In order to create a detailed MAP file for a Delphi application, make sure to have the {D+} compiler directive set, and the "Detailed Map File" option selected in the Option | Compiler linker page of the IDE.
For Borland C++ you must also have selected the "Detailed Map File" linker option, as well as the "Include Line Numbers" in the debugging page of your project options.
How?
Let's assume that we've created a detailed MAP file for our application.
This MAP file will contain, among other things, a list of error addresses and source line numbers, grouped together for each source file that is part of the application.
A few example lines from a MAP file can be seen below:
Line numbers for Findmap(FINDMAP.DPR) segment Findmap 9 0001:001F 10 0001:005B 11 0001:006B 12 0001:0083 13 0001:008ENow just say that a runtime error has occurred at $0001:$0083, then we can see from the fragment above that the corresponding source file is FINDMAP.DPR at line number 12. Actually, both Borland Pascal and Delphi have a nice feature built-in that automatically finds the right source file and line number for us. It's the Search | Find Error address in the Delphi IDE:
If you hit OK, it'll take to the correct source file at the correct line. Actually, it takes you to the previous line, but that's OK, we know that the error occurred somewhere in the neighbourhood anyway.
Borland C++
Borland C++, however, does not come with this feature.
Sure, if you debug an application from within the IDE, then you will end up at the appropriate source file and line number whenever a run-time error occurs, but most of the time an error occurs when we're not debugging.
Or a client calls us to tell that an error just occurred (and you may only hope we wrote down that address).
Since we all know that reproducing an error is just as hard as fixing it, we should be glad to have a feature that enables us to find an error by it's address!
The Engine
So, the question remains how to programmatically find the correct source file and line number in a .Map file.
That just boils down to a simple parse of the text file.
First by looking for the right segment (0001 in this case) which will identify the module and hence source file.
Then, we'll only have to check a few lines to get to the right offset.
And since the lines in a MAP file are sorted, we can even give estimates if we don't have an exact match (by keeping track of the line before and after the runtime error address value we're looking for).
User Interface
The interface should be as simple as possible.
We need a map file as input and an error address.
In case the user doesn't remember where to find the map file, let's offer him a file open dialog.
The resulting source file and line number for the runtime error address can be shown in one or two label components.
The form might look like this:
The Source
The source code for the form is give in the listing below.
unit Unitfind; {$I-} interface uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Buttons; type TForm1 = class(TForm) OpenDialog1: TOpenDialog; MapFile: TLabel; FileEdit: TEdit; ErrorAddress: TLabel; ErrorEdit: TEdit; SourceFile: TLabel; SourceLine: TLabel; BitBtn1: TBitBtn; procedure FindSourceFileAndLineNumber(Sender: TObject); procedure FormCreate(Sender: TObject); end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); begin if OpenDialog1.Execute then FileEdit.Text := OpenDialog1.FileName end; procedure TForm1.FindSourceFileAndLineNumber(Sender: TObject); var f: System.Text; LastFileName,FileName: String; PastHeader: Boolean absolute FileName; Str: String; Len: Byte absolute Str; Seg: String[6]; Off: String[4]; MinV,MaxV,NewVal,OffValue: Word; MinL,MaxL,Line,i,error: Integer; begin Str := ErrorEdit.Text; Seg := ' '+Copy(Str,1,5); Off := Copy(Str,6,4); MinV := 0; MaxV := $FFFF; MinL := 0; MaxL := 32767; NewVal := 0; Val('$'+Off,OffValue,error); Line := 0; FileName := ''; LastFileName := 'Error: no match found!'; SourceLine.Caption := ''; SourceFile.Caption := 'Searching...'; Cursor := crHourGlass; Application.ProcessMessages; System.Assign(f,FileEdit.Text); System.Reset(f); if (IOResult = 0) then while (Line = 0) and not eof(f) do begin readln(f,Str); if Pos('Line numbers for ',Str) = 1 then begin FileName := Copy(Str,Pos('(',Str)+1,Len); FileName[0] := Chr(Pos(')',FileName)-1); SourceFile.Caption := FileName; Application.ProcessMessages; Line := MinL { stop already? } end else if PastHeader then repeat i := Pos(Seg,Str); if i > 0 then { next segment hit } begin repeat Dec(i) until Str[i] = ' '; Inc(i); Line := 0; repeat Line := 10 * Line + Ord(Str[i]) - 48; Inc(i) until Str[i] = ' '; Delete(Str,1,i+5); if Pos(Off,Str) = 1 then begin SourceLine.Caption := Format('Found at line %d',[Line]); LastFileName := FileName end else begin Val('$'+Copy(Str,1,4),NewVal,error); if error = 0 then begin if (NewVal > MinV) and (NewVal <= offvalue) then begin LastFileName := FileName; MinV := NewVal; MinL := Line; SourceLine.Caption := Format('Somewhere between %d and %d',[MinL,MaxL]) end else if (NewVal < maxv) and (NewVal >= OffValue) then begin LastFileName := FileName; MaxV := NewVal; MaxL := Line; SourceLine.Caption := Format('Somewhere between %d and %d',[MinL,MaxL]) end end; Line := 0 end end until (i = 0) or (Line <> 0) { found } end; SourceFile.Caption := LastFileName; Cursor := crDefault; system.close(f); if IOResult <> 0 then { skip } end; end.
And... Action!
So, let's open up the FINDMAP.MAP file and look for a runtime error that supposed to have happened at address 0001:0083.
The answer is given immediately at line 12 of file FINDMAP.DPR.
Granted, we have to locate and open the file by ourselves, but at least now we know where to look.
Adding on-line Help Support to FindMap
The form we designed has only four active controls (controls that can get the input focus), the two edit controls, the file open dialog and the find button.
The working of this dialog and these controls might sound obvious, but sometimes we'd want to add a little helpfile for extra support or just that little touch extra that makes our application different from the rest.
In order to add helpfile support to our application we have to do three things:
2. Assign HelpFile
In order to assign a helpfile to the application, all we've got to do is to go the project options and enter the name of the helpfile.
We can use the Options | Project dialog at the application page for that:
3. Assign HelpContext IDs
The last step is again very easy.
Just walk through your four components and assign the value of each component's help context ID to the property helpcontext.
That's all there is to it.
Make sure that each component that needs on-line help gets a helpcontext property value that's bigger than zero.
The final application will now automatically load the selected winhelp file (specified in step 2) at the correct topic whenever the user hits F1.
The accompanying FindMap for Borland C++ article will show you how to use this little debugging tool by integrating it with the Borland C++ IDE for optimal usage.