使用ASM调用对象方法 - 第2部分

时间:2012-02-26 14:49:49

标签: delphi assembly delphi-2010 basm

这个问题基于previous,但这只是我们。

我已经设法让它工作了,但是,我发现了一些我不清楚的东西,所以如果有人能解释以下行为,那就太棒了。

我有以下课程:

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;

以及以下两个程序:

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;

为了测试工作版本,需要调用以下内容:

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;

并且为了测试 NOT 工作版本:

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;

最后一个问题:为什么 CallObjMethNOTWorking 工作,而 CallObjMethWorking 是?我猜测编译器如何处理TMethod有一些特别之处...但由于我的汇编知识有限,我无法理解。

如果有人能向我解释一下,我将非常感谢,谢谢!

2 个答案:

答案 0 :(得分:5)

HenrickHellström对他的answer是正确的,我注意到你的问题是用Delphi 2010标记的,因此只关注Win32。但是,你可能有兴趣看看如果你转到Win64(Delphi> = XE2)会出现什么情况,所以我在Henrick的代码中添加了一个示例Win64版本:

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;

有几个解释性说明:

1)ASM语句:在Delphi XE2 x64中没有pascal和asm代码的混合,因此编写汇编代码的唯一方法是在一个由a组成的例程中单asm..end块,无begin..end。请注意,32位asm代码周围的begin..end也会产生影响。具体来说,您正在强制生成堆栈帧,并让编译器生成函数参数的本地副本。 (如果你首先使用程序集,你可能不希望编译器这样做。)

2)调用约定: 在Win64上,只有一个调用约定。 registerstdcall之类的内容实际上毫无意义;它完全相同,Microsoft's Win64 calling convention。基本上是这样的:参数在RCXRDXR8R9寄存器(和/或XMM0-XMM4中传递,返回RAX/XMM0中的值通过引用传递大于64位的值。

被叫函数可能会使用:RAX, RCX, RDX, R8-R11, ST(0)-ST(7), XMM0-XMM5, YMM0-YMM5, YMM6H-YMM15H,并且必须保留RBX, RSI, RDI, RBP, R12-R15, XMM6-XMM15。在适当的情况下,被调用函数需要发出CLD / EMMS / VZEROUPPER指令以将CPU恢复到预期状态。

3)对齐和阴影空间 重要的是,每个函数在堆栈上都有自己的阴影空间,即使没有参数并且无论被调用的函数是否真正触及它,它至少有4个QWORD参数的堆栈空间。此外,在每个函数调用的站点(在每个CALL语句处),RSP预期为16字节对齐(对于MacOSX32上的ESP相同,顺便说一句。)。 这通常会导致类似:sub rsp, ##; call $$; add rsp, ##构造,其中##将是要调用函数的(QWORD)参数的总和,加上可选的8个字节用于RSP的对齐。请注意,RSP站点上CALL的对齐会在函数输入时导致RSP = ###8h(因为CALL会将返回地址放在堆栈上),所以假设没有人对{ {1}}在你做之前,你可以期待它。

在提供的示例中,SSE2 RSP指令用于测试MOVDQA的对齐方式。 (RSP用作目标寄存器,因为它可以自由修改但不能包含任何函数参数数据。)

4)假设 此处的代码假定编译器不插入代码来更改XMM5。可能存在这种情况可能不正确的情况,因此请注意做出这种假设。

5)异常处理 Win64中的异常处理有点复杂,应该由编译器正确完成(上面的示例代码不会这样做)。为了使编译器能够这样做,理想情况下,您的代码应使用Allen Bauer here概述的新BASM指令/伪指令RSP.PARAMS.PUSHNV。鉴于正确(错误)的情况,否则会发生不好的事情。

答案 1 :(得分:4)

Delphi Win32中的默认调用约定是&#34; register&#34;。第一个参数在EAX中传递,第二个参数在EDX中传递,第三个参数在ECX中传递。仅当有三个以上的参数或者传递大于4个字节的值类型时才使用堆栈,但在您的示例中并非如此。

您的第一个CallObjMethWorking过程有效,因为编译器在调用CallObjMethWorking时,已经在EDX中放置了aStrValue,在ECX中放置了aIntValue。但是,由于您没有清理两个推送指令,因此在程序返回时必然会发生不好的事情。

您的代码应如下所示。在这种情况下,stdcall指令是可选的,但最好将它用于这样的事情,以确保您的参数不会丢失,因为您在实际调用之前将寄存器用于其他目的。方法:

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;