Delphi记录赋值错误

7

我遇到了一些 Delphi XE3 编译器的奇怪行为(我是为 x86 架构编译的)。

假设我有一个类,其中有一个字段 - 自定义记录,包含几个简单类型的字段:

  TPage = class
  type
    TParagraph = record
    public
      FOwner:  TPage;

      FFirst:  Integer;
      FSecond: Integer;

      procedure Select;
    end;
  public
    FSelected: TParagraph;
  end;

procedure TPage.TParagraph.Select;
begin
  FOwner.FSelected:=Self;
end;

逻辑是我的页面可以包含多个段落,在某些时刻我想让其中一个段落成为选定的(以便在程序的其他部分执行一些操作):
procedure TMainForm.Button1Click(Sender: TObject);
var
  lcPage:      TPage;
  lcParagraph: TPage.TParagraph;
begin
  lcPage:=TPage.Create;
  try
    <...>

    lcParagraph.FOwner:=lcPage;
    lcParagraph.FFirst:=1;
    lcParagraph.FSecond:=2;

    lcParagraph.Select;

    <...>
  finally
    lcPage.Free;
  end;

只要我的记录大小不超过一定限制,一条引用和两个整数完全没有问题,在这种情况下,我将得到如下汇编指令:

MainUnit.pas.350: FOwner.FSelected:=Self;
00C117B3 8B45FC           mov eax,[ebp-$04]
00C117B6 8B00             mov eax,[eax]
00C117B8 8B55FC           mov edx,[ebp-$04]
00C117BB 8B0A             mov ecx,[edx]
00C117BD 894804           mov [eax+$04],ecx
00C117C0 8B4A04           mov ecx,[edx+$04]
00C117C3 894808           mov [eax+$08],ecx
00C117C6 8B4A08           mov ecx,[edx+$08]
00C117C9 89480C           mov [eax+$0c],ecx

我看到了三个正确的mov操作,将内存从本地记录复制到类中。

但是!如果我向我的记录添加更多字段,生成的汇编代码会发生变化,记录赋值不再正确。

    TParagraph = record
    public
      FOwner:  TPage;

      FFirst:  Integer;
      FSecond: Integer;
      FThird:  Integer;

      procedure Select;
    end;

MainUnit.pas.350: FOwner.FSelected:=Self;
00C117C9 8B45FC           mov eax,[ebp-$04]
00C117CC 8B55FC           mov edx,[ebp-$04]
00C117CF 8B12             mov edx,[edx]
00C117D1 8BF2             mov esi,edx
00C117D3 8D7A04           lea edi,[edx+$04]
00C117D6 A5               movsd 
00C117D7 A5               movsd 
00C117D8 A5               movsd 
00C117D9 A5               movsd 

在类 FSelected 中,我得到了垃圾数据:

enter image description here

在执行lea指令后,CPU的状态如下:

enter image description here

在这个例子中,02D37280是我的lcPage类的地址,因此02D37284应该包含它的字段-FSelected记录的开始。但是movsd指令从ESI复制内存到EDI,从02D37280到02D37284,这根本没有意义! 如果我将ESI寄存器更改为我的本地lcParagraph变量的值EAX(19F308),则复制将被正确执行:

enter image description here

我描述的是已知的错误吗?还是我对Delphi的某些基本知识缺失?这是一个分配记录的好方法吗?我可以很容易地解决问题,例如通过在procedure TPage.TParagraph.Select;中将FOwner.FSelected:=Self;更改为CopyMemory(@FOwner.FSelected, @Self, SizeOf(Self));,但我想弄清楚问题出在哪里。

最小可重现示例:

program RecordAssignmentIssue;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
  TPage = class
  type
    TParagraph = record
    public
      FOwner:  TPage;

      FFirst:  Integer;
      FSecond: Integer;
      FThird:  Integer;

      procedure Select;
    end;
  public
    FSelected: TParagraph;
  end;

procedure TPage.TParagraph.Select;
begin
  FOwner.FSelected:=Self;
end;

var
  lcPage:      TPage;
  lcParagraph: TPage.TParagraph;
begin
  try
    lcPage:=TPage.Create;
    try
      lcParagraph.FOwner:=lcPage;
      lcParagraph.FFirst:=1;
      lcParagraph.FSecond:=2;
      lcParagraph.FThird:=3;

      lcParagraph.Select;

      Assert(CompareMem(@lcPage.FSelected, @lcParagraph, SizeOf(lcParagraph)));
      // get rid of FThird and assertion will pass
    finally
      lcPage.Free;
    end;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

你能提供一个 [mcve] 吗? - David Heffernan
是的,我已经编辑了原始帖子。 - loltrol
1
我可以在XE7中重现这个问题,我认为这是一个bug。我想评论一下代码的设计看起来有点不对。我不确定这个方法是否属于记录类型。我会将其作为TPage的一个方法传递给TParagraph。这样做将解决这个问题。您还可以通过将记录类型放在类外部来解决它。所以这看起来像是嵌套类型声明的问题。我不会惊讶如果这个bug甚至存在于最新版本的Delphi中。它只在32位上出现问题,在64位上没有问题,即使记录类型更长。 - David Heffernan
1
Delphi 11 中出现了相同的错误。 - LU RD
1
@LURD 我重新测试了一下,发现是我的错误。在XE7中将TParagraph声明移动到TPage外面也会失败。 - David Heffernan
显示剩余6条评论
1个回答

5
这是一个仍然存在于Delphi 11中的bug(感谢LU RD证实)。您应该向Quality Portal提交错误报告。
同时,我认为您可以通过在TPage而不是TParagraph中进行赋值来解决这个问题。像这样:
program RecordAssignmentIssue;

{$APPTYPE CONSOLE}

uses
  System.SysUtils;

type
  TPage = class
  type
    TParagraph = record
    public
      FOwner:  TPage;

      FFirst:  Integer;
      FSecond: Integer;
      FThird:  Integer;

      procedure Select;
    end;
  private
    procedure Select(const Paragraph: TParagraph);
  public
    FSelected: TParagraph;
  end;

procedure TPage.TParagraph.Select;
begin
  FOwner.Select(Self);
end;

{ TPage }

procedure TPage.Select(const Paragraph: TParagraph);
begin
  FSelected:=Paragraph;
end;

var
  lcPage:      TPage;
  lcParagraph: TPage.TParagraph;

begin
  try
    lcPage:=TPage.Create;
    try
      lcParagraph.FOwner:=lcPage;
      lcParagraph.FFirst:=1;
      lcParagraph.FSecond:=2;
      lcParagraph.FThird:=3;

      lcParagraph.Select;

      Assert(CompareMem(@lcPage.FSelected, @lcParagraph, SizeOf(lcParagraph)));
      // get rid of FThird and assertion will pass
    finally
      lcPage.Free;
    end;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

或者另一个非常简单的解决方法就是引入一个额外的本地指针变量来保存指向 Self 的指针:

procedure TPage.TParagraph.Select;
var
  P: ^TParagraph;
begin
  P := @Self;
  FOwner.FSelected := P^;
end;

1
报告 [RSP-36156 记录分配失败] (https://quality.embarcadero.com/browse/RSP-36156) - LU RD

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