在 Delphi 中检测 VMT 或堆损坏的正确工具是什么?
-
18-09-2019 - |
题
我是使用 Delphi 2007 开发大型应用程序的团队的成员,我们怀疑堆损坏,因为有时会出现没有其他解释的奇怪错误。我相信编译器的 Rangechecking 选项仅适用于数组。我想要一个工具,当对应用程序未分配的内存地址进行写入时,该工具可以给出异常或日志。
问候
编辑:该错误的类型为:
错误:模块“BoatLogisticsAMCAtracsServer.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)
内部异常部分是重新引发异常时的调用堆栈。
编辑3: 目前的理论是虚拟内存表(VMT)在某种程度上被破坏了。当这种情况发生时,没有任何迹象表明它发生。仅当调用方法时才会引发异常(总是 地址 FFFFFFDD,十进制 -35),但为时已晚。您不知道错误的真正原因。任何有关如何捕获此类错误的提示都非常感谢!我们尝试过使用 SafeMM,但问题是即使使用 3 GB 标志,内存消耗也太高。所以现在我尝试向 SO 社区提供赏金:)
编辑4: 一个提示是,根据日志,在此之前经常(甚至总是)存在另一个异常。例如,它可以是数据库中的乐观锁定。我们尝试过强制引发异常,但在测试环境中它工作得很好。
编辑5: 故事还在继续……我搜索了过去 30 天的日志。结果:
- “读取地址 FFFFFFDB” 0
- “读取地址 FFFFFFDC”24
- “读取地址 FFFFFFDD”270
- “读取地址 FFFFFFDE”22
- “读取地址 FFFFFFDF”7
- “读取地址 FFFFFFE0” 20
- “读取地址 FFFFFFE1” 0
所以目前的理论是枚举(粗体有很多)覆盖指针。我得到了 5 次点击,上面的地址不同。这可能意味着枚举包含 5 个值,其中第二个值最常用。如果出现异常,数据库应发生回滚,并且 Bold 对象应被销毁。也许有可能并非所有内容都被破坏,并且枚举仍然可以写入地址位置。如果这是真的,也许可以通过正则表达式在代码中搜索具有 5 个值的枚举?
编辑6: 总而言之,目前还没有解决问题的办法。我意识到我可能会用调用堆栈误导您。是的,其中有一个计时器,但还有其他没有计时器的调用堆栈。对此感到抱歉。但有两个共同因素。
- 读取地址 FFFFFFxx 时出现异常。
- 调用堆栈的顶部是 System.TObject.InheritsFrom (sys\system.pas:9237)
这让我确信 维勒克 最好描述问题。我也确信问题出在 Bold 框架的某个地方。但是 大的 问题是,这样的问题如何解决?仅仅有像这样的断言是不够的 维勒克 建议,因为损坏已经发生并且调用堆栈此时已消失。因此,描述一下我对可能导致错误的原因的看法:
- 在某个地方,指针被分配了错误值 1,但它也可以是 0、2、3 等。
- 一个对象被分配给该指针。
- 对象基类中有方法调用。这会导致调用 TObject.InheritsForm 方法,并在地址 FFFFFFDD 上出现异常。
这 3 个事件可以一起出现在代码中,但也可能在以后使用。我认为最后一个方法调用也是如此。
编辑7: 我们与 Bold Jan Norden 的作者密切合作,他最近在 Bold 框架的 OCL 评估器中发现了一个错误。当这个问题得到解决后,此类异常会减少很多,但它们仍然偶尔会出现。但令人欣慰的是,这个问题几乎已经解决了。
解决方案
我没有解决方案,但有一些关于该特定错误消息的线索。
System.TObject.InheritsFrom 从自指针(类)中减去常量 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 中尝试过,并得到“读取地址 FFFFFFD1”,因为 Delphi 版本之间的 vmtParent 不同。
问题是这种情况发生在 Bold 框架的深处,因此您可能无法在应用程序代码中防范它。
您可以在 DMAttracsTimers 代码(我假设是您的应用程序代码)中使用的对象上尝试此操作:
Assert(Integer(Obj.ClassType)<>1,'Corrupt vmt');
其他提示
听起来对象实例数据的内存已损坏。
VMT 本身并没有被损坏,FWIW:VMT(通常)存储在可执行文件中,并且映射到它的页面是只读的。相反,正如 VilleK 所说,您的情况下实例数据的第一个字段似乎被值为 1 的 32 位整数覆盖。这很容易验证:检查方法调用失败的对象的实例数据,并验证第一个双字是否为00000001。
如果确实是实例数据中的 VMT 指针被损坏,那么我将如何找到损坏它的代码:
确保有一种不需要用户输入的自动方法来重现问题。由于 Windows 如何选择内存布局,该问题可能只能在一台计算机上重现,而无需在重现之间重新启动。
重现该问题并记下内存损坏的实例数据的地址。
重新运行并检查第二次复制:确保第二次运行中损坏的实例数据的地址与第一次运行的地址相同。
现在,进入第三次运行,在前两次运行指示的内存部分上放置一个 4 字节数据断点。重点是要中断对此内存的每次修改。至少一个中断应该是填充 VMT 指针的 TObject.InitInstance 调用;可能还有其他与实例构造相关的内容,例如内存分配器中;在最坏的情况下,相关的实例数据可能已经从之前的实例中回收了内存。要减少所需的单步执行量,请使数据断点记录调用堆栈,但不实际中断。通过在虚拟调用失败后检查调用堆栈,您应该能够找到错误的写入。
麦吉当然是对的。(fastmm4 调用标志 fulldebugmode 或类似的东西)。
请注意,这通常适用于定期检查堆分配之前和之后的障碍(在每次 heapmgr 访问时?)。
这有两个后果:
- fastmm 检测到错误的位置可能与发生错误的位置不同
- 可能无法检测到完全随机写入(不是现有分配的溢出)。
所以这里还有一些需要考虑的事情:
- 启用运行时检查
- 查看所有编译器的警告。
- 尝试使用不同的delphi版本或FPC进行编译。其他编译器/rtls/堆管理器具有不同的布局,这可能导致更容易捕获错误。
如果这一切都没有产生任何结果,请尝试简化应用程序,直到它消失。然后调查最近评论/ifdefed 的部分。
我要做的第一件事就是将 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 已分配,但事实并非如此。
我注意到堆栈跟踪中有一个计时器。
我见过很多奇怪的错误,其原因是在释放表单后触发了计时器事件。
原因是计时器事件可能会被放在消息队列上,并且不会被处理以销毁其他组件。
解决该问题的一种方法是禁用计时器作为销毁表单的第一个条目。禁用时间调用 Application.processMessages 后,因此在销毁组件之前会处理任何计时器事件。
另一种方法是检查表单是否在计时器事件中被破坏。(组件状态中的 csDestroying)。
可以贴一下这个程序的源代码吗?
boldsystem.tboldmember.calculaulderivedmemberwithexpression(boldsystem.pas:4016)
这样我们就可以看到 4016 行发生了什么。
还有这个函数的CPU视图?
(只需在此过程的第 4016 行设置断点并运行。如果遇到断点,则复制+粘贴 CPU 视图内容)。
这样我们就可以看到地址00404E78处是哪条CPU指令。
可重入代码会不会有问题?
尝试在 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;
氮@
我认为还有另一种可能:触发计时器以检查是否存在“悬空登录会话”。然后,对 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。
问题不是“_GetMayDropSession”引用已释放的会话变量吗?
我以前见过这种错误,在 TMS 中,对象被释放并在 onchange 等中引用(仅在某些情况下它给出错误,非常困难/不可能重现,现在已由 TMS 修复:-))。另外,在 RemObjects 会话中,我得到了类似的东西(由于我自己的错误编程错误)。
我会尝试向会话类添加一个虚拟变量并检查它的值:
- 公共变量 iMagicNumber:整数;
- 构造函数创建:iMagicNumber := 1234567;
- 析构函数销毁:iMagicNumber := -1;
- “其他程序”:断言(iMagicNumber = 1234567)