Assembly calls to System unit functions on FreePascal x64
Asked Answered
A

2

6

I have some Delphi/assembly code that compiles and works fine (XE2) for Win32, Win64, and OSX 32. However, since I need it to work on Linux, I have been looking at compiling FPC versions of it (so far, Win32/64, Linux32/64).

By and large, it works well, but the one thing I have not been able to get to work are calls/jumps to Delphi System unit functions, like such:

  jmp System.@FillChar

This appears to have the desired effect on FPC Win32/Linux32, but fails with an exception on FPC Win64/Linux64. (I am quite familiar with the calling convention differences among the platforms, so don't think that's the reason.)

What is the correct way of doing this on FPC for x64 platforms?

[Edit1] --- In response to David's comment, here is a simplified program that illustrates the problem (at least I hope it does so accurately):

program fpcx64example;
{$IFDEF FPC}
  {$MODE DELPHI}
  {$ASMMODE INTEL}
{$ELSE}
  {$APPTYPE CONSOLE}
{$ENDIF}

procedure FillMemCall (p: pointer; len: longword; val: byte);
asm
  // this function and the System function have the same parameters
  // in the same order -- they are already in their proper places here
  jmp System.@FillChar
end;

function MakeString (c: AnsiChar; len: longword): AnsiString;
begin
  Setlength (Result, len);
  if len > 0 then FillMemCall (PAnsiChar(Result), len, byte(c));
end;

begin
  try
    writeln (MakeString ('x',10));
  except
    writeln ('Exception!');
  end;
end.

To compile with FPC: [Win32:] fpc.exe fpcx64example.dpr, [Win64:] ppcrossx64.exe fpcx64example.dpr, [Linux32:] fpc.exe -Tlinux -XPi386-linux- -FD[path]\FPC\bin\i386-linux fpcx64example.dpr, [Linux64:] ppcrossx64.exe -Tlinux -XPx86_64-linux- -FD[FPCpath]\bin\x86_64-linux fpcx64example.dpr.

Works fine with Delphi (Win32/64). For FPC, removing jmp System.@FillChar above gets rid of the exception on x64.

The solution (Thanks to FPK):

Delphi and FPC do not generate stack frames for functions under the exact same conditions, so that the RSP register may have a different alignment in the versions compiled by the two. The solution is to avoid this difference. One way of doing so, for the FillMemCall example above, would look like such:

{$IFDEF CPU64} {$DEFINE CPUX64} {$ENDIF} // for Delphi compatibility
procedure FillMemCall (p: pointer; len: longword; val: byte);
  {$IFDEF FPC} nostackframe; {$ENDIF} //Force same FPC behaviour as in Delphi
asm
  {$IFDEF CPUX64}
    {$IFNDEF FPC} .NOFRAME {$ENDIF} // To make it explicit (Delphi)...
    // RSP = ###0h at the site of the last CALL instruction, so
    // since the return address (QWORD) was pushed onto the stack by CALL,
    // it must now be ###8h -- if nobody touched RSP.
    movdqa xmm0, dqword ptr [rsp + 8] // <- Testing RSP misalignment -- this will crash if not aligned to DQWORD boundary
  {$ENDIF}
  jmp System.@FillChar
end;

This isn't exactly beautiful, but it now works for Win/Linux 32/64 for both Delphi and FPC.

Accommodate answered 15/5, 2013 at 8:6 Comment(5)
Some might wonder why, when it has the same parameters as FillChar in the same order, FillMemCell exists at all. Why not just call FillChar?Europa
There seems to be an issue with FillChar or Writeln implementation in x64 as it raises an exception if you call it directly without the MakeString function.Plummet
StringOfChar seems to do what your function does already.Europa
@RobKennedy My guess is that this is heavily cut down and that the real code perhaps has good reason for doing what appears here to be pointlessBelize
@RobKennedy Yes indeed, the code above is pretty pointless. But as David suggested, it's been cut down just to reproduce and illustrate the problem.Accommodate
T
8

Short answer: the correct way to do this is using a call instruction.

Long answer: x86-64 code requires that the stack is 16 byte aligned, so FillMemCall contains at the entry point a compiler generated sub rsp,8 and an add rsp,8 at the exit (the other 8 bytes are added/remove by the call/ret pair). Fillchar is on the other hand hand-coded assembler and uses the nostackframe directive so it does not contain a compiler generated sub/add pair and as soon fillchar is left, the stack is messed up because FillChar does not contain an add rsp,8 before the ret instruction.

Workarounds like using the nostackframe directive for FillMemCall or adjusting the stack before doing the jmp might be possible but are subject to be broken by any future compiler change.

Tamarisk answered 15/5, 2013 at 15:55 Comment(5)
Perfect! That explains it. Delphi implicitly does the same as .NOFRAME here (no stack frame generated), while FPC does not... Just didn't think about that possibility...Accommodate
Exactly. This is a clear violation of the x86-64 calling conventions.Enjambment
@WarrenP Not per se. There is no requirement that every function needs a stack frame (leaf functions can often get away without. NB I am NOT talking about shadow space), it's just a difference in the compilers. At the asm level, it's your responsibility to take care of these things anyway.Accommodate
@WarrenP Are you sure? Such straight redirection is used e.g. during dynamic linking of a library, e.g. when you call the OS APIs. A plain jmp can be safe, if no stack frame is involved, and exceptions are mapped as expected in the destination code, as expected by Win64.Bromidic
Oh I see. As in a jump table. My bad. True.Enjambment
B
3

The easiest is to get rid of assembler in this case, and use only pascal code:

procedure FillMemCall (p: pointer; len: longword; val: byte); inline; 
begin
  fillchar(p^,len,val);
end;

And it will work with both FPC and Delphi (for newer versions where inline is known).

And it will work on all platforms and CPU (even arm).

And it will be faster than the asm jmp @System.FillChar end trick since the procedure is declared as inline: no code will be generated, and calling FillMemCall will directly call fillchar, that is it will generated the following code:

function MakeString (c: AnsiChar; len: longword): AnsiString;
begin
  Setlength (Result, len);
  if len > 0 then 
    fillchar(pointer(Result)^, len, c);
end;
Bromidic answered 15/5, 2013 at 17:45 Comment(3)
I agree with this in principle, but the above code is a simplification of more complex code where this is simply not an option. (There is a reason for the asm and why I bother)Accommodate
@Accommodate So you should better create a new question with the correct code. Dealing with stack frames is very specific to code involved. For instance, it will depend on the local variables, how much parameters are passed, and if the function returns some variable - including handling of reference counted kind of variables...Bromidic
Again, agreed in principle. But my issue is resolved, and when I asked the question I just wasn't aware that that was the issue at hand (and I had indicated above the fact that it was a simplification). Anyway, thanks for your feedback. Btw, great work you do.Accommodate

© 2022 - 2024 — McMap. All rights reserved.