什么是在Delphi中检测VMT或堆损坏的正确工具?

6
我是一个团队的成员,我们使用Delphi 2007开发一个大型应用程序,我们怀疑存在堆污染问题,因为有时出现奇怪的错误,没有其他解释。我认为编译器的Rangechecking选项仅适用于数组。我想要一个工具,在应用程序写入未分配的内存地址时,能够给出异常或日志记录。
问候
编辑:错误类型为:
错误:在模块“BoatLogisticsAMCAttracsServer.exe”中访问地址00404E78。读取地址FFFFFFDD
编辑2:感谢所有的建议。不幸的是,我认为解决方案比这更深入。我们使用了Bold for Delphi的补丁版本,因为我们拥有源代码。可能在Bold框架中引入了一些错误。是的,我们有一个由JCL处理的带有调用堆栈和跟踪消息的日志。因此,带有异常的调用堆栈可能如下所示:
20091210 16:02:29 (2356) [EXCEPTION] Raised EBold: Failed to derive ServerSession.mayDropSession: Boolean
OCL expression: not active and not idle and timeout and (ApplicationKernel.allinstances->first.CurrentSession <> self)
Error: Access violation at address 00404E78 in module 'BoatLogisticsAMCAttracsServer.exe'. Read of address FFFFFFDD. At Location BoldSystem.TBoldMember.CalculateDerivedMemberWithExpression (BoldSystem.pas:4016)

Inner Exception Raised EBold: Failed to derive ServerSession.mayDropSession: Boolean
OCL expression: not active and not idle and timeout and (ApplicationKernel.allinstances->first.CurrentSession <> self)
Error: Access violation at address 00404E78 in module 'BoatLogisticsAMCAttracsServer.exe'. Read of address FFFFFFDD. At Location BoldSystem.TBoldMember.CalculateDerivedMemberWithExpression (BoldSystem.pas:4016)
Inner Exception Call Stack:
 [00] System.TObject.InheritsFrom (sys\system.pas:9237)

Call Stack:
 [00] BoldSystem.TBoldMember.CalculateDerivedMemberWithExpression (BoldSystem.pas:4016)
 [01] BoldSystem.TBoldMember.DeriveMember (BoldSystem.pas:3846)
 [02] BoldSystem.TBoldMemberDeriver.DoDeriveAndSubscribe (BoldSystem.pas:7491)
 [03] BoldDeriver.TBoldAbstractDeriver.DeriveAndSubscribe (BoldDeriver.pas:180)
 [04] BoldDeriver.TBoldAbstractDeriver.SetDeriverState (BoldDeriver.pas:262)
 [05] BoldDeriver.TBoldAbstractDeriver.Derive (BoldDeriver.pas:117)
 [06] BoldDeriver.TBoldAbstractDeriver.EnsureCurrent (BoldDeriver.pas:196)
 [07] BoldSystem.TBoldMember.EnsureContentsCurrent (BoldSystem.pas:4245)
 [08] BoldSystem.TBoldAttribute.EnsureNotNull (BoldSystem.pas:4813)
 [09] BoldAttributes.TBABoolean.GetAsBoolean (BoldAttributes.pas:3069)
 [10] BusinessClasses.TLogonSession._GetMayDropSession (code\BusinessClasses.pas:31854)
 [11] DMAttracsTimers.TAttracsTimerDataModule.RemoveDanglingLogonSessions (code\DMAttracsTimers.pas:237)
 [12] DMAttracsTimers.TAttracsTimerDataModule.UpdateServerTimeOnTimerTrig (code\DMAttracsTimers.pas:482)
 [13] DMAttracsTimers.TAttracsTimerDataModule.TimerKernelWork (code\DMAttracsTimers.pas:551)
 [14] DMAttracsTimers.TAttracsTimerDataModule.AttracsTimerTimer (code\DMAttracsTimers.pas:600)
 [15] ExtCtrls.TTimer.Timer (ExtCtrls.pas:2281)
 [16] Classes.StdWndProc (common\Classes.pas:11583)

内部异常部分是在重新引发异常时的调用堆栈。

EDIT3: 目前的理论是虚拟内存表(VMT)出现了某些问题。当这种情况发生时,没有任何迹象。只有在调用方法时才会引发异常(始终在地址FFFFFFDD,即-35十进制),但此时已经太晚了。您不知道错误的真正原因。非常感谢任何有关如何捕获此类错误的提示!!!我们已尝试使用SafeMM,但即使使用3 GB标志,内存消耗也太高。所以现在我试图向SO社区提供悬赏:)

EDIT4: 一个提示是根据日志,通常(甚至总是)在此之前会有另一个异常。例如,可能是数据库中的乐观锁定。我们已经尝试强制引发异常,但在测试环境中它仍然正常工作。

EDIT5: 故事还在继续......我现在对过去30天的日志进行了搜索。结果:

  • "读取地址 FFFFFFDB" 0
  • "读取地址 FFFFFFDC" 24
  • "读取地址 FFFFFFDD" 270
  • "读取地址 FFFFFFDE" 22
  • "读取地址 FFFFFFDF" 7
  • "读取地址 FFFFFFE0" 20
  • "读取地址 FFFFFFE1" 0

目前的理论是,一个枚举类型(在 Bold 中有很多)覆盖了一个指针。我得到了五个不同地址的结果。这可能意味着该枚举类型包含五个值,其中第二个值最常用。如果发生异常,则应回滚数据库并销毁 Boldobjects。也许有一种可能性,即并非所有内容都被销毁,而某个枚举类型仍然可以写入地址位置。如果这是真的,那么可能可以通过正则表达式搜索具有五个值的枚举类型的代码?

编辑6:总结一下,目前还没有解决问题的方法。我意识到我的调用堆栈可能会误导你。是的,其中有一个计时器,但也有其他没有计时器的调用堆栈。对此我感到抱歉。但有两个共同因素。

  • 一个地址为 FFFFFFxx 的读取异常。
  • 调用堆栈的顶部是 System.TObject.InheritsFrom(sys\system.pas:9237)

这使我相信VilleK最好描述了问题。 我也相信问题出现在 Bold 框架中。 但是,重要的问题是,如何解决此类问题? 像VilleK建议的断言是不够的,因为损坏已经发生,此时调用堆栈已经消失。因此,为了描述可能导致错误的内容:

  1. 某个指针被分配了错误的值 1,但也可以是 0、2、3 等。
  2. 将对象分配给该指针。
  3. 对象的基类中有方法调用。这会导致调用方法 TObject.InheritsForm 并在地址 FFFFFFDD 上出现异常。

这三个事件可以在代码中同时出现,但它们也可以在以后使用。我认为这对于最后一个方法调用是正确的。

编辑7: 我们与 Bold Jan Norden 的作者密切合作,他最近在 Bold 框架的 OCL 评估器中发现了一个错误。当这个错误被修复后,这种异常情况大大减少,但仍然偶尔发生。但是几乎解决了这个问题,这是一个很大的解脱。

1
请告诉我们,您的程序中是否出现了访问冲突对话框或者只是意外的逻辑行为。我之所以这样问,是因为如果您有一个指针指向“某个地方”的随机内容,那么您很可能会遇到访问冲突错误。您应该非常幸运才能够访问随机地址并且仅仅得到逻辑错误。 - Maksee
正如我在上一次编辑中写的那样,我们认为VMT已经损坏,可能是由于指针损坏,因此当应用程序调用虚拟方法时,它会引发异常。 - Roland Bengtsson
这是一个多线程应用程序吗? - Nat
你能告诉我们,BOLD中那个bug具体是什么吗? - Alex
你的意思是源代码改变了吗? 我不知道我能透露多少,因为Bold仍然是闭源的。 错误修复在BoldOCLEvaluator.pas文件中的TBoldOclEvaluatorVisitor.VisitTBoldOclOperation过程中进行。如果有一个OCL表达式如下:'invoice.isPayed and (invoice.invoiceNo > 1000)'那么Bold会使用称为短路评估的东西。 如果invoice.isPayed为true,则(invoice.invoiceNo > 1000)永远不会被评估,但在解析树中仍然存在对它的引用。 因此,通过以下方式将其设置为nil来修复错误:if not ie.OwnsValue then ie.SetReferenceValue(nil); - Roland Bengtsson
显示剩余2条评论
10个回答

6

您写道,如果应用程序未分配内存地址上进行写操作,则希望出现异常。

但实际上,无论是硬件还是操作系统都会确保这种情况发生。

如果您的意思是要检查应用程序分配的地址范围内的无效内存写入,那么您所能做的就只有那么多了。您应该使用FastMM4,并在应用程序的调试模式下以其最详细和偏执的设置来使用它。这将捕获许多无效的写入,对已释放的内存的访问等,但它无法捕获所有问题。考虑一个指向另一个可写内存位置(如大型字符串或浮点值数组的中间)的悬空指针 - 写入将成功,并且它将破坏其他数据,但是内存管理器无法捕获此类访问。


2
根据您对分配的定义,情况可能有所不同。只有在访问未从操作系统分配的内存时,CPU/操作系统才会出现错误。OP谈到的分配内存是指从堆管理器中分配的内存。这是有区别的。 - Marco van de Voort

5
我没有解决方案,但有一些关于这个特定错误消息的线索。
System.TObject.InheritsFrom函数从Self指针(类)中减去常量vmtParent以获取指向父类地址的指针。
在Delphi 2007中,vmtParent被定义为:
vmtParent = -36;
因此,错误$FFFFFFDD(-35)听起来像是在这种情况下,类指针为1。
以下是一个复现它的测试案例:
procedure TForm1.FormCreate(Sender: TObject);
var
  I : integer;
  O : tobject;
begin
  I := 1;
  O := @I;
  O.InheritsFrom(TObject);
end;

我已经在Delphi 2010中尝试过,但由于vmtParent在不同的Delphi版本之间不同,所以出现了“Read of address FFFFFFD1”的问题。
问题在于这发生在Bold框架的深处,因此您可能会在应用程序代码中难以防范此问题。
您可以在用于DMAttracsTimers代码(我假设这是您的应用程序代码)中使用此方法来测试对象:
Assert(Integer(Obj.ClassType)<>1,'Corrupt vmt');

另一个有用的答案,但同意粗体中的错误很难捕捉到,因为它无法再现。 - Roland Bengtsson
我相信你给出了最有用的答案。不幸的是,你建议的断言只有在为时已晚时才会触发。坏指针已经被分配到某个地方了。老实说,我还没有一个好的现实想法来解决这个问题... - Roland Bengtsson
谢谢。关于这个问题,很难再做出进一步的猜测了。你提到你已经打了Bold的补丁,可能引入了错误。也许你可以详细比较一下你的补丁和原始代码,并仔细寻找错误,在更改中添加断言等,以确保没有问题。此外,如果“RemoveDanglingLogonSessions”经常出现在崩溃堆栈中,请搜索你的代码,查找创建/销毁登录会话的位置,也许你在某个地方销毁了一个实例,但没有从登录会话列表中删除它。 - Ville Krumlinde

3
看起来你的对象实例数据出现了内存损坏。
VMT本身并没有被破坏,顺便说一下:VMT(虚方法表)通常存储在可执行文件中,映射到它的页面是只读的。相反,正如VilleK所说,在你的情况下,实例数据的第一个字段似乎被一个值为1的32位整数覆盖。这很容易验证:检查调用失败的方法的对象的实例数据,并验证第一个dword是否为00000001。
如果确实是实例数据中的VMT指针被损坏,那么我将介绍如何找到损坏它的代码:
1.确保有一种自动化的方式来重现该问题,不需要用户输入。由于Windows可能选择如何布置内存,因此该问题可能仅在单个计算机上重现,而不需要重新启动。
2.重现该问题并注意内存被损坏的实例数据的地址。
3.重新运行并检查第二次复制:确保第二次运行中被损坏的实例数据的地址与第一次运行中的地址相同。
4.现在,进入第三次运行,在前两次运行指示的内存部分上放置一个4字节的数据断点。关键是在每次修改此内存时中断。至少有一个断点应该是TObject.InitInstance调用,它填充VMT指针;可能会有其他与实例构造相关的断点,例如在内存分配器中;在最坏的情况下,相关的实例数据可能是来自先前实例的回收内存。为了减少所需步骤的数量,使数据断点记录调用堆栈,但不实际中断。通过在虚拟调用失败后检查调用堆栈,您应该能够找到错误的写入。

不幸的是,这个问题无法重现,但您的答案仍可以为问题添加一些信息。该错误在生产中可能会发生5-20次每周。如果有一个可重现的情况,那么情况就会容易得多(但并不简单...)。我在日志中看到一个提示是,在这个错误之前有一个异常,请参见问题中的edit4。 - Roland Bengtsson
我同意Barry的观点。我会在每个可能会受到损坏的主要类的顶部添加一个FMagicFlag1:Int64字段,并在底部添加一个FMagicFlag2:Int64字段,然后在构造函数中设置FMagicFlag1=cMagic1,FMagicFlag2=cMagic2,在该类的某个重要方法中使用Assert(FMagicFlag1=cMagic1)/Assert(FMagicFlag2=cMagic2)。通过这种方式检测实例数据的损坏情况。 - Warren P

2

mghie是正确的。 (fastmm4调用标志fulldebugmode或类似的内容)。

请注意,这通常与在堆分配之前和之后定期检查的屏障一起工作。(在每个heapmgr访问时?)

这有两个后果:

  • fastmm检测到错误的位置可能偏离发生错误的位置
  • 完全随机写入(不是现有分配的溢出)可能不会被检测到。

因此,以下是需要考虑的其他事项:

  • 启用运行时检查
  • 审查编译器的所有警告。
  • 尝试使用不同的Delphi版本或FPC进行编译。其他编译器/ rtls / heapmanagers具有不同的布局,这可能更容易捕获错误。

如果所有这些都没有效果,请简化应用程序,直到问题消失。然后调查最近的注释/ ifdefed部分。


2
我会做的第一件事是将MadExcept添加到您的应用程序中,并获得一个堆栈回溯,打印出精确的调用树,这将让您对发生了什么有一些想法。您需要看到一个调用树,其中包括栈中所有参数和局部变量的值,而不是随机异常和二进制/十六进制内存地址。
如果我怀疑结构中存在内存损坏,而该结构对我的应用程序至关重要,我通常会编写额外的代码以使跟踪此错误成为可能。
例如,在内存结构(类或记录类型)中可以排列Magic1:Word在每个记录的开头,Magic2:Word在内存中的结尾。通过查看每个记录的Magic1和Magic2是否没有从构造函数中设置的值更改,完整性检查函数可以检查这些结构的完整性。析构函数会将Magic1和Magic2更改为其他值,例如$FFFF。
我还会考虑在我的应用程序中添加跟踪日志。Delphi应用程序中的跟踪日志通常始于我声明一个TraceForm表单,其中包含一个TMemo,而TraceForm.Trace(msg:String)函数最初为"Memo1.Lines.Add(msg)"。随着我的应用程序越来越成熟,跟踪日志设施是我观察运行中应用程序的方式,以了解它们的整体行为和异常情况。然后,当发生“随机”崩溃或内存损坏,并且“没有解释”的情况时,我有一个跟踪日志可以回顾并查看是什么导致了这种特殊情况。
有时不是内存损坏,而是简单的基本错误(我忘记检查X是否被分配,然后我去引用它:X.DoSomething(...),假设X已经被分配,但实际上没有)。

我发现了这篇文章https://dev59.com/CEjSa4cB1Zd3GeqPEly5。我不知道FastMM可以定期或者在每次操作后扫描内存池。非常有趣! - Roland Bengtsson

1

我注意到堆栈跟踪中有一个计时器。
我见过很多奇怪的错误,其中原因是计时器事件在释放表单后被触发。
原因是计时器事件可能被放入消息队列中,在其他组件销毁之前不会被处理。
解决这个问题的一种方法是在表单销毁的第一个条目中禁用计时器。在禁用计时器后调用Application.processMessages,以便在销毁组件之前处理任何计时器事件。
另一种方法是在timerevent中检查表单是否正在销毁(componentstate中的csDestroying)。


Benny在这里提出了一个很好的观点。计时器可能是各种故障的根源。尝试用使用后台线程和TTHread.Synchronize而不是WIN32计时器消息的TJvThreadTimer替换TTimer! - Warren P

0

可重入代码可能存在问题吗?

尝试在TTimer事件处理程序代码周围放置一些守卫代码:

procedure TAttracsTimerDataModule.AttracsTimerTimer(ASender: TObject);
begin
  if FInTimer then
  begin
    // Let us know there is a problem or log it to a file, or something. 
    // Even throw an exception
    OutputDebugString('Timer called re-entrantly!'); 
    Exit; //======> 
  end;

  FInTimer := True;
  try

    // method contents

  finally
    FInTimer := False;
  end;
end;

N@


当然一切皆有可能,但我更相信枚举理论。请参见上面的第5次编辑(Edit5)。 - Roland Bengtsson

0
我认为还有另一种可能性:计时器被触发以检查是否存在“悬空登录会话”。然后,对TLogonSession对象进行调用以检查它是否可以被丢弃(_GetMayDropSession),对吗?但是如果该对象已经被销毁呢?也许是由于线程安全问题或仅仅是一个.Free调用而不是FreeAndNil调用(因此变量仍然<> nil)等等。同时,其他对象被创建,因此内存得到重用。如果您稍后尝试访问变量,则可能会收到随机错误...
一个例子:
procedure TForm11.Button1Click(Sender: TObject);
var
  c: TComponent;
  i: Integer;
  p: pointer;
begin
  //create
  c := TComponent.Create(nil);
  //get size and memory
  i := c.InstanceSize;
  p := Pointer(c);
  //destroy component
  c.Free;
  //this call will succeed, object is gone, but memory still "valid"
  c.InheritsFrom(TObject);
  //overwrite memory
  FillChar(p, i, 1);
  //CRASH!
  c.InheritsFrom(TObject);
end;

在模块“Project10.exe”中,地址004619D9处发生访问冲突。读取地址01010101。


0

你能发布这个过程的源代码吗?

BoldSystem.TBoldMember.CalculateDerivedMemberWithExpression (BoldSystem.pas:4016)

这样我们就可以看到4016行发生了什么。

还有这个函数的CPU视图吗?
(只需在此过程的4016行上设置断点并运行。如果您遇到断点,请复制+粘贴CPU视图内容)。
这样我们就可以看到地址00404E78处的CPU指令是什么。


在4016行,除了try except之后的raise EBold.Create(s)之外,通常不会到达该行。因此,通常不会出现这种错误。我无法在测试中重现它。但是有一些进展,请参见我的编辑。 - Roland Bengtsson

0

问题不是"_GetMayDropSession"引用了一个已释放的会话变量吗?

我以前见过这种错误,在TMS中,对象被释放并在onchange等中引用(只有在某些情况下才会出现错误,非常困难/无法重现,现在由TMS修复了:-))。还有RemObjects会话,我遇到了类似的问题(由于我的糟糕编程错误)。

我会尝试向会话类添加一个虚拟变量并检查其值:

  • 公共变量iMagicNumber:整数;
  • 构造函数create:iMagicNumber:= 1234567;
  • 析构函数destroy:iMagicNumber:= -1;
  • "其他程序":assert(iMagicNumber = 1234567)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接