Question

This question is based on a previous, but that's just FYI.

I've managed to get it working, however, I've found something that's not clear to me, so if anyone can explain the following behaviour, it would be awesome.

I have the following class:

type
  TMyObj = class
  published
    procedure testex(const s: string; const i: integer);
  end;

procedure TMyObj.testex(const s: string; const i: integer);
begin
  ShowMessage(s + IntToStr(i));
end;

and the following two procedures:

procedure CallObjMethWorking(AMethod: TMethod; const AStrValue: string; const AIntValue: Integer);
begin
  asm
    PUSH DWORD PTR AIntValue;
    PUSH DWORD PTR AStrValue;
    CALL AMethod.Code;
  end;
end;

procedure CallObjMethNOTWorking(AInstance, ACode: Pointer; const AStrValue: string; const AIntValue: Integer);
begin
  asm
    MOV EAX, AInstance;
    PUSH DWORD PTR AIntValue;
    PUSH DWORD PTR AStrValue;
    CALL ACode;
  end;
end;

In order to test the working version, one needs to call the following:

procedure ...;
var
  LObj: TMyObj;
  LMethod: TMethod;
  LStrVal: string;
  LIntVal: Integer;
begin
  LObj := TMyObj.Create;
  try
    LMethod.Data := Pointer( LObj );
    LMethod.Code := LObj.MethodAddress('testex');

    LStrVal := 'The year is:' + sLineBreak;
    LIntVal := 2012;

    CallObjMethWorking(LMethod, LStrVal, LIntVal);
  finally
    LObj.Free;
  end; // tryf
end;

and in order to test the NOT working version:

procedure ...;
var
  LObj: TMyObj;
  LCode: Pointer;
  LData: Pointer;
  LStrVal: string;
  LIntVal: Integer;
begin
  LObj := TMyObj.Create;
  try
    LData := Pointer( LObj );
    LCode := LObj.MethodAddress('testex');

    LStrVal := 'The year is:' + sLineBreak;
    LIntVal := 2012;

    CallObjMethNOTWorking(LData, LCode, LStrVal, LIntVal);
  finally
    LObj.Free;
  end; // tryf
end;

And finally the question: why isn't CallObjMethNOTWorking working, while CallObjMethWorking is? I'm guessing that there's something special in how the compiler treats TMethod... but since my assembly knowledge is limited, I can't understand it.

I would highly appreciate if someone could explain this to me, thank you!

Was it helpful?

Solution

Default calling convention in Delphi Win32 is "register". The first parameter is passed in EAX, the second in EDX and the third in ECX. The stack is only used if there are more than three parameters, or if value types larger than 4 bytes are passed, but that is not the case in your example.

Your first CallObjMethWorking procedure works because the compiler has already placed aStrValue in EDX and aIntValue in ECX when CallObjMethWorking was called. However, since you are not cleaning up your two push instructions, bad things are bound to happen when the procedure returns.

Your code should look like this. The stdcall directive is optional in this case, but it might be a good idea to use it for things like this to make sure your parameters aren't lost because you use the registers for other purposes before you get around to actually call the method:

procedure CallObjMeth(AInstance, ACode: Pointer; const AStrValue: string; const AIntValue: Integer); stdcall;
asm
  MOV EAX, AInstance;
  MOV EDX, DWORD PTR AStrValue;
  MOV ECX DWORD PTR AIntValue;
  CALL ACode;
end;

OTHER TIPS

Henrick Hellström is correct with his answer, and I notice that your question is tagged with Delphi 2010 and thus only concerns Win32. However, you might be interested to see what the situation would look like if you move forward to Win64 (Delphi >= XE2), so I added an example Win64 version to Henrick's code:

procedure CallObjMeth(AInstance, ACode: Pointer; const AStrValue: string; const AIntValue: Integer); stdcall;
asm
{$IFDEF CPU386}
  MOV EAX, AInstance;
  MOV EDX, DWORD PTR AStrValue;
  MOV ECX, DWORD PTR AIntValue;
  {$IFDEF MACOS}
   //On MacOSX32 ESP = #######Ch here       
   SUB ESP, 0Ch  
  {$ENDIF}     
  CALL ACode;
  {$IFDEF MACOS}
   ADD ESP, 0Ch // restoring stack
  {$ENDIF}     
{$ENDIF}
{$IFDEF CPUX64}{$IFDEF WIN64} // <- see comments
  .NOFRAME //Disable stack frame generation
  //MOV RCX, AInstance {RCX} //<- not necessary because AInstance already is in RCX
  MOV R10, ACode {RDX}
  MOV RDX, AStrValue {R8}
  MOV R8D, AIntValue {R9D}
  SUB RSP, 28h    //Set up stack shadow space and align stack: 4*8 bytes for 4 params + 8 bytes bytes for alignment
  {$IFNDEF DO_NOT_TEST_STACK_ALIGNMENT}
  MOVDQA XMM5, [RSP]  //Ensure that RSP is aligned to DQWORD boundary -> exception otherwise
  {$ENDIF}
  CALL R10 //ACode
  ADD RSP, 28h  //Restore stack
{$ENDIF}{$ENDIF}
end;

There are several explanatory notes to make:

1) ASM statement: In Delphi XE2 x64 there is no mixing of pascal and asm code, so the only way to write assembly code is in a routine that is comprised of a single asm..end block, no begin..end. Note that the begin..end around your 32-bit asm code also does have an effect. Specifically, you are forcing the generation of a stack frame and let the compiler make local copies of the function parameters. (If you resort to using assembly in the first place, you would might not want the compiler to do that.)

2) Calling convention: On Win64, there is only a single calling convention. Things like register and stdcall are effectively meaningless; it is all the same, Microsoft's Win64 calling convention. It is essentially this: parameters are passed in RCX, RDX, R8 and R9 registers (and/or XMM0-XMM4, return values in RAX/XMM0. Larger than 64-bit values are passed via reference.

Called functions may use: RAX, RCX, RDX, R8-R11, ST(0)-ST(7), XMM0-XMM5, YMM0-YMM5, YMM6H-YMM15H, and must preserve RBX, RSI, RDI, RBP, R12-R15, XMM6-XMM15. Where appropriate, called functions need to issue CLD/EMMS/VZEROUPPER instructions to restore the CPU to the expected state.

3) Alignment and shadow space Importantly, each function has its own shadow space on the stack, which is at least 4 QWORD param's worth of stack space, even if there are no params and irrespective of whether the called function actually touches it. Moreover, at the site of each function call (at each CALL statement), RSP is expected to be 16-byte aligned (same for ESP on MacOSX32, btw.). This often leads to things like: sub rsp, ##; call $$; add rsp, ## constructs in which ## would be the sum of the (QWORD) parameters with which the function is to be called, plus an optional 8 bytes for alignment of RSP. Note that the alignment of RSP at the CALL site results in RSP = ###8h upon function entry (because CALL puts the return address on the stack), so assuming nobody messes with RSP before you do, you can expect it to be that.

In the example provided, the SSE2 MOVDQA instruction is used to test the alignment of RSP. (XMM5 is used as destination register because it can be freely modified yet cannot contain any function parameter data).

4) Assumptions The code here assumes that the compiler does not insert code to change RSP. There may be situations in which this may not be true, so beware of making this assumption.

5) Exception handling Exception handling in Win64 is a bit complicated and should properly be done by the compiler (the example code above does not do this). To allow the compiler to do so, ideally your code should use the new BASM directives/pseudo-instructions .PARAMS, .PUSHNV and .SAVENV as outlined by Allen Bauer here. Given the right (wrong) situation, bad things could happen otherwise.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top