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

Com*_*sNo 3 delphi assembly delphi-2010 basm

这个问题基于之前的问题,但这仅仅是FYI.

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

我有以下课程:

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;
Run Code Online (Sandbox Code Playgroud)

以及以下两个程序:

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;
Run Code Online (Sandbox Code Playgroud)

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

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;
Run Code Online (Sandbox Code Playgroud)

并且为了测试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;
Run Code Online (Sandbox Code Playgroud)

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

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

Phi*_*hiS 5

HenrickHellström对他的回答是正确的,我注意到你的问题是用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;
Run Code Online (Sandbox Code Playgroud)

有几个解释性说明:

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

2)调用约定:在Win64上,只有一个调用约定.事情就像register并且stdcall实际上毫无意义; 微软的Win64调用约定完全相同.它本质上是这样的:参数传递中RCX,RDX,R8R9寄存器(和/或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字节对齐(对于ESPMacOSX32,btw.).这通常会导致类似于以下内容的sub rsp, ##; call $$; add rsp, ##构造:##将是要调用函数的(QWORD)参数的总和,以及用于对齐的可选8字节RSP.需要注意的是对齐RSPCALL网站导致RSP = ###8h在函数入口(因为CALL把堆栈上的返回地址),因此假设没有与弄乱RSP你这样做之前,你可以期望它是.

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

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

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