6

我是一个将 Delphi 2007 用于大型应用程序的团队的成员,我们怀疑堆损坏,因为有时会出现没有其他解释的奇怪错误。我相信编译器的 Rangechecking 选项仅适用于数组。我想要一个工具,当在应用程序未分配的内存地址上写入时,它会给出异常或日志。

问候

编辑:错误类型:

错误:模块“BoatLogisticsAMCAttracsServer.exe”中地址 00404E78 的访问冲突。读取地址 FFFFFFDD

EDIT2:感谢所有建议。不幸的是,我认为解决方案比这更深。由于我们拥有源代码,因此我们为 Delphi 使用了 Bold 的补丁版本。可能在 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

所以目前的理论是一个枚举(有很多粗体)覆盖一个指针。我在上面得到了 5 个不同地址的点击。这可能意味着枚举包含 5 个值,其中第二个是最常用的。如果出现异常,则应为数据库进行回滚,并且应销毁 Boldobjects。也许不是所有东西都被破坏了,枚举仍然可以写入地址位置。如果这是真的,也许可以通过正则表达式搜索代码以查找具有 5 个值的枚举?

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

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

这让我相信VilleK最能描述这个问题。我也确信问题出在 Bold 框架的某个地方。但最大的问题是,如何解决这样的问题?仅仅有一个像VilleK这样的断言是不够的,因为损坏已经发生并且调用堆栈在那一刻已经消失了。因此,描述我对可能导致错误的原因的看法:

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

这 3 个事件可以在代码中一起使用,但也可以在以后使用。我认为这对于最后一个方法调用是正确的。

EDIT7:我们与 Bold Jan Norden 的作者密切合作,他最近在 Bold 框架中的 OCL 评估器中发现了一个错误。修复此问题后,这些异常减少了很多,但它们仍然偶尔会出现。但这是一个很大的安慰,这几乎解决了。

4

10 回答 10

6

你写你希望有一个例外,如果

在未由应用程序分配的内存地址上写入

但无论如何,硬件操作系统都会确保这一点。

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

于 2009-12-10T10:41:20.293 回答
5

我没有解决方案,但有一些关于该特定错误消息的线索。

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”,因为 vmtParent 在 Delphi 版本之间是不同的。

问题是这发生在 Bold 框架的深处,因此您可能无法在应用程序代码中防范它。

您可以在 DMAttracsTimers 代码(我假设是您的应用程序代码)中使用的对象上尝试此操作:

Assert(Integer(Obj.ClassType)<>1,'Corrupt vmt');
于 2010-02-01T08:40:15.863 回答
3

听起来您的对象实例数据存在内存损坏。

VMT 本身没有损坏,FWIW:VMT(通常)存储在可执行文件中,映射到它的页面是只读的。相反,正如 VilleK 所说,在您的案例中,实例数据的第一个字段看起来被一个值为 1 的 32 位整数覆盖。这很容易验证:检查其方法调用失败的对象的实例数据,并验证第一个 dword 是 00000001。

如果确实是实例数据中的 VMT 指针被损坏,那么我将通过以下方式找到损坏它的代码:

  1. 确保有一种无需用户输入即可重现问题的自动化方法。由于 Windows 可能选择布局内存的方式,该问题可能只能在单个机器上重现,而无需在重现之间重新启动。

  2. 重现问题并记下内存损坏的实例数据的地址。

  3. 重新运行并检查第二次复制:确保在第二次运行中损坏的实例数据的地址与第一次运行的地址相同。

  4. 现在,进入第三次运行,在前两次运行指示的内存段上放置一个 4 字节数据断点。关键是要打破对这个记忆的每一次修改。至少一个中断应该是填充 VMT 指针的 TObject.InitInstance 调用;可能还有其他与实例构造相关的,例如在内存分配器中;在最坏的情况下,相关的实例数据可能已经从以前的实例中回收了内存。要减少所需的步进量,请让数据断点记录调用堆栈,但实际上不中断。通过在虚拟调用失败后检查调用堆栈,您应该能够找到错误的写入。

于 2010-02-01T09:40:58.627 回答
2

当然,mghie 是对的。(fastmm4 调用标志 fulldebugmode 或类似的东西)。

请注意,这通常适用于定期检查堆分配之前和之后的障碍(在每次 heapmgr 访问时?)。

这有两个后果:

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

所以这里还有一些其他的事情需要考虑:

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

如果这一切都没有产生,请尝试简化应用程序,直到它消失。然后调查最新的评论/ifdefed 部分。

于 2009-12-10T12:46:34.300 回答
2

我要做的第一件事就是将 MadExcept 添加到您的应用程序中,并获得一个堆栈回溯,打印出确切的调用树,这将使您了解这里发生了什么。您需要查看调用树,而不是随机异常和二进制/十六进制内存地址,其中包含堆栈中所有参数和局部变量的值。

如果我怀疑对我的应用程序至关重要的结构中的内存损坏,我通常会编写额外的代码来使跟踪这个错误成为可能。

例如,在内存结构(类或记录类型)中,可以安排在内存中的每条记录的开头有一个 Magic1:Word,在结尾有一个 Magic2:Word。完整性检查功能可以检查这些结构的完整性,方法是查看每个记录 Magic1 和 Magic2 是否未更改它们在构造函数中设置的内容。析构函数会将 Magic1 和 Magic2 更改为其他值,例如 $FFFF。

我也会考虑在我的应用程序中添加跟踪日志记录。delphi 应用程序中的跟踪日志记录通常从我声明一个 TraceForm 表单开始,上面有一个 TMemo,TraceForm.Trace(msg:String) 函数以“Memo1.Lines.Add(msg)”开始。随着我的应用程序的成熟,跟踪记录工具是我观察运行应用程序以了解其行为和不当行为的整体模式的方式。然后,当发生“无解释”的“随机”崩溃或内存损坏时,我有一个跟踪日志可以查看,看看是什么导致了这种特殊情况。

有时不是内存损坏,而是简单的基本错误(我忘了检查是否分配了 X,然后我去取消引用它:假设 X 已分配的 X.DoSomething(...) ,但事实并非如此。

于 2009-12-10T18:47:05.677 回答
1

我注意到堆栈跟踪中有一个计时器。
我见过很多奇怪的错误,原因是在我释放表单后触发了计时器事件。
原因是可以在消息que上放置一个定时器事件,并且noge得到处理以破坏其他组件。
解决该问题的一种方法是禁用计时器作为销毁表单的第一个条目。禁用时间调用 Application.processMessages 后,因此在销毁组件之前处理任何计时器事件。
另一种方法是在 timerevent 中检查表单是否正在销毁。(组件状态中的csDestroying)。

于 2010-02-01T10:18:06.093 回答
0

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

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

所以我们可以看到第 4016 行发生了什么。

还有这个函数的CPU视图?
(只需在此过程的第 4016 行设置断点并运行。如果遇到断点,则复制+粘贴 CPU 视图内容)。
所以我们可以看到哪条 CPU 指令在地址 00404E78 处。

于 2010-02-01T19:17:50.233 回答
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@

于 2010-02-02T00:06:18.140 回答
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。

于 2010-02-03T07:13:19.217 回答
0

“_GetMayDropSession”不是引用释放的会话变量的问题吗?

我以前见过这种错误,在 TMS 中,对象在 onchange 等中被释放和引用(仅在某些情况下它给出了错误,非常难以/不可能重现,现在由 TMS 修复:-))。同样在 RemObjects 会话中,我得到了类似的东西(由于我自己的编程错误)。

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

  • 公共变量 iMagicNumber:整数;
  • 构造函数创建:iMagicNumber := 1234567;
  • 析构函数销毁:iMagicNumber := -1;
  • “其他程序”:assert(iMagicNumber = 1234567)
于 2010-02-08T07:31:19.357 回答