我遇到了一些 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](https://istack.dev59.com/TH3DC.webp)
在执行lea指令后,CPU的状态如下:
![enter image description here](https://istack.dev59.com/SM9Gj.webp)
在这个例子中,02D37280是我的lcPage类的地址,因此02D37284应该包含它的字段-FSelected记录的开始。但是movsd指令从ESI复制内存到EDI,从02D37280到02D37284,这根本没有意义! 如果我将ESI寄存器更改为我的本地lcParagraph变量的值EAX(19F308),则复制将被正确执行:
![enter image description here](https://istack.dev59.com/V6n9A.webp)
我描述的是已知的错误吗?还是我对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.
TPage
的一个方法传递给TParagraph
。这样做将解决这个问题。您还可以通过将记录类型放在类外部来解决它。所以这看起来像是嵌套类型声明的问题。我不会惊讶如果这个bug甚至存在于最新版本的Delphi中。它只在32位上出现问题,在64位上没有问题,即使记录类型更长。 - David HeffernanTParagraph
声明移动到TPage
外面也会失败。 - David Heffernan