Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

delphi - local variable and array of TPair<Int,Int> - strange behavior of memory allocation

i have following code example compiled in delphi xe5 update 2.

procedure TForm1.FormCreate(Sender: TObject);
var i,t:Integer;
    buf: array [0..20] of TPair<Integer,Integer>;
begin
  t := 0;
  for i := Low(buf) to High(buf) do begin
    ShowMessage(
      Format(
        'Pointer to i = %p;'#$d#$a+
        'Pointer to buf[%d].Key = %p;'#$d#$a+
        'Pointer to buf[%d].Value = %p;'#$d#$a+
        'Pointer to t = %p',
        [@i, i, @(buf[i].Key), i, @(buf[i].Value), @t]
      )
    );
    buf[i].Key := 0;
    buf[i].Value := 0;
    t := t + 1;
  end;
end;

if i run it it shows me the adresses of the variables. the variables i and t have adresses in the memory range of buf!
when ireaches 3 the assignment buf[i].Value := 0; overwrite first 3 bytes of i and the last byte of t. this results in a infinity loop because i gets allways reset to 0 when it reaches 3.
if i allocate the memory by myself with SetLength(buf,20); everything is fine.

the picture shows what i mean.

Output, Memory Adresses

my setup:

  • Windows 7 64 Bit
  • Delphi XE 5 Update 2
  • Debug Configuration 32 Bit

strange, isn't it?
can anybody reproduce it?
is it a bug in the delphi compiler?

thanks.

EDIT:
here is the same example, but maybe better to understand what i mean: memory areas

and btw.: sorry for my bad english ;)

like image 341
linluk Avatar asked Aug 12 '14 10:08

linluk


2 Answers

This definitely looks like a compiler bug. It only affects an array of TPair allocated on the stack. For example, this compiles and runs fine :

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Generics.Collections;

var i:Integer;
    buf: array [0..20] of TPair<Integer,Integer>;
begin
  for i := Low(buf) to High(buf) do begin
    buf[i].Key := 0;
    buf[i].Value := 0;
  end;
end.

This, however, demonstrates the error :

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Generics.Collections;    

procedure DoSomething;
var i:Integer;
    buf: array [0..20] of TPair<Integer,Integer>;
begin
  for i := Low(buf) to High(buf) do begin
    buf[i].Key := 0;
    buf[i].Value := 0;
  end;
end;

begin
  DoSomething;
end.

The compiler seems to be miscalculating the size of a TPair<Integer,Integer>. The compiled assembly shows the preamble as follows :

Project1.dpr.14: begin
00445C50 55               push ebp
00445C51 8BEC             mov ebp,esp
00445C53 83C4E4           add esp,-$1c  //***  Allocate only 28 bytes (7words)
Project1.dpr.15: for i := Low(buf) to High(buf) do begin
00445C56 33C0             xor eax,eax
00445C58 8945FC           mov [ebp-$04],eax
Project1.dpr.16: buf[i].Key := 0;
00445C5B 8B45FC           mov eax,[ebp-$04]
00445C5E 33D2             xor edx,edx
00445C60 8954C5E7         mov [ebp+eax*8-$19],edx
Project1.dpr.17: buf[i].Value := 0;
00445C64 8B45FC           mov eax,[ebp-$04]
00445C67 33D2             xor edx,edx
00445C69 8954C5EB         mov [ebp+eax*8-$15],edx
Project1.dpr.18: end;
00445C6D FF45FC           inc dword ptr [ebp-$04]
Project1.dpr.15: for i := Low(buf) to High(buf) do begin
00445C70 837DFC15         cmp dword ptr [ebp-$04],$15
00445C74 75E5             jnz $00445c5b
Project1.dpr.19: end;
00445C76 8BE5             mov esp,ebp
00445C78 5D               pop ebp
00445C79 C3               ret 
00445C7A 8BC0             mov eax,eax

The compiler has only allocated 7 dwords on the stack. The first is for the integer i, leaving only 6 dwords allocated for the TPair array, which is insufficient (SizeOf(TPair<integer,integer>) is equal to 8 -> two dwords). On the third iteration, mov [ebp+eax*8-$15],edx (ie: buf[2].Value) runs into the stack location for i and sets its value to zero.

You can demonstrate a working program by forcing enough space on the stack :

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Generics.Collections;


procedure DoSomething;
var i:Integer;
    fixalloc : array[0..36] of Integer; // dummy variable
                                        // allocating enough space for
                                        // TPair array
    buf: array [0..20] of TPair<Integer,Integer>;
begin
  for i := Low(buf) to High(buf) do begin
    buf[i].Key := i;
    buf[i].Value := i;
  end;
end;

begin
  DoSomething;
end.

This is tested in XE2, but it seems this persists through at least XE5 if you are also seeing the problem.

like image 66
J... Avatar answered Oct 11 '22 01:10

J...


It is clear that @J... is correct in identifying this as a compiler bug. From my tests I observe that it afflicts both 32 and 64 bit Windows versions of the compiler. I don't know about the OSX compiler, or the mobile compilers.

There are some reasonable workarounds available. This problem produces reasonable output:

{$APPTYPE CONSOLE}
uses
  System.SysUtils, Generics.Collections;

type
  TFixedLengthPairArray = array [0..20] of TPair<Integer,Integer>;

procedure DoSomething;
var
  i: Integer;
  buf: TFixedLengthPairArray;
begin
  Writeln(Format('%p %p', [@i, @buf]));
end;

begin
  DoSomething;
end.

Likewise this one:

{$APPTYPE CONSOLE}
uses
  System.SysUtils, Generics.Collections;

type
  TFixedLengthPairArray = array [0..20] of TPair<Integer,Integer>;

procedure DoSomething;
var
  i: Integer;
  buf: array [0..20] of TPair<Integer,Integer>;
begin
  Writeln(Format('%p %p', [@i, @buf]));
end;

begin
  DoSomething;
end.

Or indeed this:

{$APPTYPE CONSOLE}
uses
  System.SysUtils, Generics.Collections;

type
  TPairOfIntegers = TPair<Integer,Integer>;

procedure DoSomething;
var
  i: Integer;
  buf: array [0..20] of TPairOfIntegers;
begin
  Writeln(Format('%p %p', [@i, @buf]));
end;

begin
  DoSomething;
end.

And even this:

{$APPTYPE CONSOLE}
uses
  System.SysUtils, Generics.Collections;

type
  TPairOfIntegers = TPair<Integer,Integer>;

procedure DoSomething;
var
  i: Integer;
  buf: array [0..20] of TPair<Integer,Integer>;
begin
  Writeln(Format('%p %p', [@i, @buf]));
end;

begin
  DoSomething;
end.

And this:

{$APPTYPE CONSOLE}
uses
  System.SysUtils, Generics.Collections;

procedure DoSomething;
type
  TPairOfIntegers = TPair<Integer,Integer>;
var
  i: Integer;
  buf: array [0..20] of TPair<Integer,Integer>;
begin
  Writeln(Format('%p %p', [@i, @buf]));
end;

begin
  DoSomething;
end.

So it seems that so long as the compiler has already instantiated the generic type before it encounters the local variable declaration, it is able to reserve the correct stack size.

like image 24
David Heffernan Avatar answered Oct 10 '22 23:10

David Heffernan