Delphi 记录分配错误

lol*_*rol 7 delphi delphi-xe3 delphi-10.4-sydney

我遇到了 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;
Run Code Online (Sandbox Code Playgroud)

逻辑是我的页面可以包含多个段落,在某些时候我希望选择其中一个段落(以便能够在程序的其他部分中使用它执行一些操作):

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;
Run Code Online (Sandbox Code Playgroud)

当我的记录不超过一定大小时,一切都可以。一个引用和两个整数就可以了,在这种情况下,我得到如下汇编指令:

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
Run Code Online (Sandbox Code Playgroud)

我可以看到三个正确的 movs 将内存从本地记录复制到类。

但!如果我向记录中添加更多字段,生成的 asm 代码会发生更改,并且记录分配将不再正确执行。

    TParagraph = record
    public
      FOwner:  TPage;

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

      procedure Select;
    end;
Run Code Online (Sandbox Code Playgroud)
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 
Run Code Online (Sandbox Code Playgroud)

然后我在班级记录 FSelected 中得到垃圾: 在此输入图像描述

lea指令后CPU状态如下:

在此输入图像描述

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

我所描述的是已知错误吗?或者我错过了delphi的一些基本知识?这是分配记录的好方法吗?我可以轻松解决该问题,例如,通过更改FOwner.FSelected:=Self;CopyMemory(@FOwner.FSelected, @Self, SizeOf(Self));in procedure TPage.TParagraph.Select;。但我想弄清楚出了什么问题。

最小可重现示例:

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.
Run Code Online (Sandbox Code Playgroud)

Dav*_*nan 5

这是 Delphi 11 中仍然存在的错误(感谢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.
Run Code Online (Sandbox Code Playgroud)

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

procedure TPage.TParagraph.Select;
var
  P: ^TParagraph;
begin
  P := @Self;
  FOwner.FSelected := P^;
end;
Run Code Online (Sandbox Code Playgroud)