解析map文件中AccessViolation的地址

Resolve address of AccessViolation in the map file

我的一个用户报告了一些罕见的 AccessViolations,我想对其进行分析。

我有那个 Build 的源代码,所以我可以创建一个 MAP 文件。但是我不知道如何在MAP文件中找到AccessViolation提供的地址。

(将来,我们希望使用像 JclDebug 这样的框架来创建可用的堆栈跟踪)。

我已经设置了一个例子:

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;

访问冲突是:

Address 004146CF. Reading from address B2D88B53 .

在地图文件中,我发现以下内容:

 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

为什么 AV 说地址 004146CF,而 MAP 文件说 0001:00055498?

即使我减去 CODE 段的起始地址 (0001),我仍然得到 004146CF-00401000 = 136CF,这也不是我要找的。

我也尝试通过搜索字符串“:00414”来查找错误地址,但没有找到任何内容。

如何从 MAP 文件中的 AV 查找地址?

详细地图文件应包含一个按标识符名称排序的部分和另一个按地址排序的部分(实际上是按 RVA,相对虚拟地址)。您将物理地址转换为 RVA 的技术是正确的。只需转到按地址排序的部分,找到最接近但不超过 $136cf 的地址。那应该是发生崩溃的函数。

但是,您确实需要确保使用调试 DCU 进行构建。否则,您只会看到程序部分的地址,即 您的 代码。

对于 DLLs/Packages,它们很可能加载到 PE 文件中指定的默认地址以外的地址。在这种情况下,您需要找到该特定模块的基地址。打开模块视图(Ctrl-Alt-M 或视图|调试视图|模块...)。查找基地址最接近但不大于崩溃地址的模块。该模块的地址将是 "base address"。使用此值计算 RVA,然后转到该模块的 MAP 文件查找位置。

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

004146CF是运行时崩溃的代码指令的实际内存地址,而.map文件中的地址是 relative 因为进程的 actual 加载地址在编译时未知。

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

0001 不是地址,更不是起始地址。它只是在 .map 文件顶部为给定段定义的 ID 号。 0001:00055498 引用标识为 0001.

的段内的相对地址 00055498

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

通常一个进程的加载地址为0000(实际值在Project Options中定义,默认为0000),但是由于各种原因,例如 re-basing,在运行时可能会有所不同。一旦确定了 actual 加载地址,就需要在进程中包含代码段的实际偏移量。该偏移量通常是 00(实际值在已编译的可执行文件的 PE header 中定义)。因此,要在运行时将内存地址映射到 .map 文件中的地址,您通常 从运行时内存地址中减去 1000。值可能不同!

在这种情况下,结果值 136CF 将是您要使用 .map 文件中的 0001 代码段查找的项目。您不太可能找到 EXACT 匹配项,因为崩溃的代码最有可能出现在函数的 中间 中,而很少出现在函数的开头功能。所以你会寻找一个 .map 项目,其起始地址最接近 136CF 但不超过它。

您没有显示整个 .map 文件,因此您的代码段中没有接近 136CF 的项目。但真正的崩溃并不像您所期望的那样发生在 CrashMe 本身。它实际上在 CrashMe() 内部调用的另一个函数中。设置 TMemo.Text 属性 调用 TWinControl.SetText(),后者调用 TControl.GetText(),后者调用 TWinControl.GetTextLen(),当试图访问 FHandleFText 无效的数据成员 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;

在诊断 AV 时,如果要将崩溃映射到 CrashMe(),仅具有 AV 的内存地址是不够的,因为该内存地址不在 CrashMe() 内本身。您需要一个指向 AV 的完整堆栈跟踪,以显示 CrashMe() 在某个时刻被调用并进行了导致实际 AV 的后续调用。 .map 文件不会帮助您获得堆栈跟踪,您需要一个在崩溃时处理该问题的运行时库,例如 JclDebug、MadExcept、EurekaLog 等。