Search code examples
delphidebuggingdebug-symbols

Resolve address of AccessViolation in the map file


One of my users has reported a few rare AccessViolations which I want to analyze.

I have the source code of exactly that Build, so I could create a MAP file. But I don't know how to find the address provided by the AccessViolation in the MAP file.

(In future, we want to use a framework like JclDebug to create useable stacktraces).

I have setup an example:

procedure CrashMe;
var
  k: TMemo; a: TButton;
begin
  k.Text := 'abc';
  k.Color := clBlack;
  k.Assign(a);
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  CrashMe;
end;

The access violation is:

Address 004146CF. Reading from address B2D88B53 .

In the map file, I find following contents:

 Start         Length     Name                   Class
 0001:00401000 000556A8H .text                   CODE
 0002:00457000 00000770H .itext                  ICODE
 0003:00458000 00001B0CH .data                   DATA
 0004:0045A000 00004CCCH .bss                    BSS
 0005:00000000 00000038H .tls                    TLS

 ....

 0001:000552F0       Unit1..TForm1
 0001:00055498       Unit1.CrashMe
 0004:00004CC8       Unit1.Form1
 0001:000554C8       Unit1.TForm1.Button1Click

Why does the AV say address 004146CF, while the MAP file says 0001:00055498 ?

Even if I subtract the start address of the CODE segment (0001), I still get 004146CF-00401000 = 136CF , which is not what I am looking for either.

I also tried to find the error adress by searching the string ":00414", but it didn't find anything.

How can I lookup the address from the AV in the MAP file?


Solution

  • Why does the AV say address 004146CF, while the MAP file says 0001:00055498 ?

    004146CF is the actual memory address of the code instruction that crashed at runtime, whereas addresses in the .map file are relative since the actual load address of the process is not known at compile time.

    Even if I subtract the start address of the CODE segment (0001)

    0001 is not an address, let alone a starting address. It is merely an ID number defined at the top of the .map file for a given segment. 0001:00055498 refers to relative address 00055498 within the segment identified as 0001.

    I still get 004146CF-00401000 = 136CF , which is not what I am looking for either.

    Usually the load address of a process is $400000 (the actual value is defined in the Project Options and is $400000 by default), but that may be different at runtime due to various reasons, such as re-basing. Once you determine the actual load address, you need to include the actual offset of the code segment within the process. That offset is usually $1000 (the actual value is defined in the compiled executable's PE header). So, to map a memory address at runtime to an address in the .map file, you usually subtract $401000 from the runtime memory address. Values may be different!

    In this case, the resulting value 136CF would be the item that you want to look for withing the 0001 code segment in the .map file. You are not likely to find an EXACT match since the code that crashed is most likely in the middle of a function and rarely at the very beginning of the function. So you would look for a .map item whose starting address is closest to 136CF without exceeding it.

    You did not show the entire .map file, so there is no item in your snippet that is close to 136CF. But the actual crash is not in CrashMe itself, like you are expecting. It is actually inside of another function that CrashMe() calls internally. Setting the TMemo.Text property calls TWinControl.SetText(), which calls TControl.GetText(), which calls TWinControl.GetTextLen(), which crashes when trying to access the FHandle or FText data member of an invalid TMemo object:

    procedure TWinControl.SetText(const Value: TCaption);
    begin
      if GetText <> Value then // <-- here
      begin
        if WindowHandle <> 0 then
          Perform(WM_SETTEXT, 0, string(Value))
        else
          FText := Value;
        Perform(CM_TEXTCHANGED, 0, 0);
      end;
    end;
    
    function TControl.GetText: TCaption;
    {$IF DEFINED(CLR)}
    begin
      Result := GetTextPiece(GetTextLen);
    end;
    {$ELSE}
    var
      Len: Integer;
    begin
      Len := GetTextLen; // <-- here
      SetString(Result, PChar(nil), Len);
      if Len <> 0 then
      begin
        Len := Len - GetTextBuf(PChar(Result), Len + 1);
        if Len > 0 then
          SetLength(Result, Length(Result) - Len);
      end;
    end;
    {$IFEND}
    
    function TWinControl.GetTextLen: Integer;
    begin
      if WindowHandle <> 0 then // <-- here
        Result := Perform(WM_GETTEXTLENGTH, 0, 0)
      else
        Result := Length(FText); // <-- or here
    end;
    

    When diagnosing an AV, if you want to map the crash to CrashMe(), it is not enough to have the memory address of the AV, since that memory address is not inside of CrashMe() itself. You need a full stack trace leading up to the AV to show that CrashMe() was called at some point and made subsequent calls that caused the actual AV. A .map file will not help you get a stack trace, you need a runtime library that handles that at the time of the crash, such as JclDebug, MadExcept, EurekaLog, etc.