FastMM4 报告 "块尾已被破坏"。

5

在我们的Delphi 7应用程序中,我有这个函数,在不包括FastMM4 v4.99时很好用。一旦包括进来,FastMM4会引发以下错误消息:"快速内存管理器在FreeMem操作期间检测到错误。块底部已损坏。"执行会在FreeMem行中停止。

function BinaryFieldToArrayOfWord( aBinaryField   : TVarBytesField;
                                   out aArrValues : TArrWord ) : Boolean;
var
  p : Pointer;
begin
  if not aBinaryField.IsBlob then
  begin
    GetMem( p, aBinaryField.DataSize );     
    try
      if aBinaryField.GetData( p ) then         
      begin
        // do something
      end;
    finally
      FreeMem( p, aBinaryField.DataSize );
    end;
  end; // if
end;

起初我认为这个函数一定有漏洞,但实际上它与Delphi 7帮助文档中的TField.GetData方法示例几乎相同:

{ Retrieve the "raw" data from Field1 }
with Field1 do
begin
  if not IsBlob  { this does not work for BLOB fields }
  begin
    { Allocate space }
    GetMem(MyBuffer, DataSize);
    try
      if not GetData(MyBuffer) then
         MessageDlg(DisplayName + ' is NULL', mtInformation, [mbOK], 0)
      else 
         { Do something with the data };
    finally
      { Free the space }
      FreeMem(MyBuffer, DataSize);
    end;
  end;
end;

我在互联网上发现,上述错误消息通常是因为数据的空间不足所致。因此,我增加了存储器块的大小,错误消息消失了,函数按预期工作。经过一些实验,我发现只需要2个字节即可满足要求。

    GetMem( p, aBinaryField.DataSize + 2 );

    FreeMem( p, aBinaryField.DataSize + 2 );

虽然这个解决方案解决了我的问题,但我对它并不完全放心,因为我不知道它的背景。最好能知道这个消息的原因。FastMM4是否需要额外的2个字节作为自己的页脚?
更新:经过一天的调试,我现在相信Delphi 7 TField.GetData方法中存在一个错误。它会在分配的内存块之外写入2个零字节。我尝试了使用FastMM4和不使用FastMM4,两种情况下它都会发生覆盖(因此不是FastMM4的错误)。我还尝试将字段强制类型转换为TVarBytesField,没有任何区别。 这里是我用详细注释写的代码,其中包含结果。 我唯一剩下的问题是:他们在后来的Delphi中是否纠正了这个错误?
  procedure TfrmMain_PBC_TH.btnDEBUGClick(Sender: TObject);
  type
    TArrBytes = array of Byte;  
  var
    p       : Pointer;
    qryTest : TADOQuery;
  begin
    qryTest := TADOQuery.Create( Application );
    try
      // The type of the TQM_BinaryData.BinData column in the MSSQL database is a       varbinary(7900). 
      // Load the #168 binary data record. It contains exactly 7900 bytes, and the value of each byte is 255
      qryTest.Connection := MainConn;
      qryTest.SQL.Add('SELECT [BinData] FROM [TQM_BinaryData] WHERE [Id] = 168');
      qryTest.Open;

      // Allocate the memory block for this.
      GetMem( p, qryTest.FieldByName('BinData').DataSize );
      // DataSize is 7902 because all TVarBytesFields have 2 byte prefix in Delphi, containing the data length.
      // So the size of the allocated memory block is 7902 bytes - we are correct so far.
      try
        // Values of the first four bytes beyond the end of the memory block (memory thrash at this point) before GetData:
        // TArrBytes(p)[7902] = 96
        // TArrBytes(p)[7903] = 197
        // TArrBytes(p)[7904] = 219
        // TArrBytes(p)[7905] = 43

        // Critical point: get the data from the field with the Delphi GetData method
        qryTest.FieldByName('BinData').GetData( p );

        // Values after GetData:
        // TArrBytes(p)[0]    = 220    TArrBytes(p)[0] and TArrBytes(p)[1] contains the length of the binary data
        // TArrBytes(p)[1]    = 30     it is correct as 30 * 256 + 220 = 7900
        // TArrBytes(p)[2]    = 255    actual data starts
        // TArrBytes(p3[2]    = 255    
        // ...
        // TArrBytes(p)[7900] = 255
        // TArrBytes(p)[7901] = 255    actual data ends
        // TArrBytes(p)[7902] = 0      changed from 96!
        // TArrBytes(p)[7903] = 0      changed from 197!
        // TArrBytes(p)[7904] = 219    no change
        // TArrBytes(p)[7905] = 43     no change
      finally
        // Here FastMM4 throws the block footer corrupt error because GetData modified the 2 bytes after the allocated memory block 
        FreeMem( p );
      end;

      qryTest.Close;
    finally
      qryTest.Free;
    end;
  end;

添加数据断点后,调用堆栈如下:

    @FillChar(???,???,???)
    TDataSet.DataConvert($7D599770,$12F448,$7D51F7F0,True)
    VarToBuffer
    TCustomADODataSet.GetFieldData($7D599770,$7D51F7F0,True)
    TField.GetData($7D51F7F0,True)
    TfrmMain_PBC_TH.btnDEBUGClick($7FF7A380)
    TControl.Click
    TButton.Click
1个回答

5
FastMM在您分配的块的末尾分配一些内存并写入已知值。然后,当您释放时,FastMM检查这些值是否符合预期。如果不是,则会引发您看到的错误,因为FastMM知道您的代码正在超出内存块的末尾。
因此,try/finally块中的某些内容正在超出内存块的末尾进行编写。这就是为什么增加其大小会删除FastMM错误的原因。如果没有看到该代码,我们无法确定出现了什么问题,您需要进行一些调试来解决问题。
您非常正确地关注您的“解决方案”。试错从来都不是编程的合理方式。您必须找出程序为什么会超出内存块的末尾进行编写。
其中一种方法是为该块末尾紧接着的地址设置数据断点。这将强制调试器在编写超出内存块末尾的代码处中断。
另外,您不需要传递第二个参数给FreeMem。这样做会使您的代码更难维护,并且完全没有任何目的。只需将指针传递给FreeMem即可。

在调试期间,我将try/finally之间的所有内容都注释掉了,除了aBinaryField.GetData(p)这一行,因此我的最终代码看起来与上面完全相同。从FreeMem中删除第二个参数并没有解决这个问题。但是,如果FastMM在我分配的内存块的末尾写入一些已知值,然后我用另一组数据完全填充该块(GetData IMHO会这样做),那么即使我只使用了分配的块,已知值也会被覆盖,不是吗?因此,+2字节是为了那些FastMM已知的值。 - Almandine
我并没有说FreeMem的第二个参数会解决问题,只是觉得传递它是毫无意义的。有些东西正在超出内存块的边界写入。 - David Heffernan
但是如果FastMM在我分配的内存块末尾写入一些已知值,然后我用另一些数据(GetData就是这样做的)完全填充该块,那么即使我只使用了分配的块,已知值也将被覆盖,不是吗?实际上并不是这样。发生的情况是,您请求10个字节,FastMM分配14个字节。它给您指向此块开头的指针,您认为其中有10个字节。但是,它随后在末尾的额外4个字节中写入已知值。如果覆盖它们,FastMM已经发现了一个错误。某些内容正在超出末尾进行写入。 - David Heffernan
我更新了原始帖子,并附上了更多的测试结果。 - Almandine
qryTest.FieldByName('BinData').DataSize 的值是多少? - David Heffernan
现在是7902,所以它计算了+2个字节。我必须分配7904字节以避免出现错误消息。 - Almandine

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