3

这个问题是基于以前的,但这只是仅供参考。

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

我有以下课程:

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;

为了测试工作的版本:

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 的处理方式有些特殊……但由于我的汇编知识有限,我无法理解。

如果有人能向我解释一下,我将不胜感激,谢谢!

4

2 回答 2

6

Henrick Hellströ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;

有几个解释性说明:

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

2) 调用约定:在 Win64 上,只有一个调用约定。类似registerstdcall的东西实际上是没有意义的;都是一样的,微软的 Win64 调用约定。本质上是这样的:参数在 , 和寄存器中传递RCXRDX和/或R8在中返回值。大于 64 位的值通过引用传递。R9XMM0-XMM4RAX/XMM0

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

3) 对齐和阴影空间 重要的是,每个函数在堆栈上都有自己的阴影空间,即使没有参数,也不管被调用的函数是否实际接触到它,它至少是 4 个 QWORD 参数的堆栈空间。此外,在每个函数调用的位置(在每个CALL语句处),RSP预计将是 16 字节对齐的(ESP在 MacOSX32 上相同,顺便说一句。)。这通常会导致以下情况:sub rsp, ##; call $$; add rsp, #### 将是调用函数的 (QWORD) 参数的总和,加上可选的 8 个字节用于对齐的RSP. 请注意,RSPCALL站点的对齐会导致RSP = ###8h函数输入(因为CALL将返回地址放在堆栈上),所以假设RSP在你这样做之前没有人弄乱,你可以期待它是那样的。

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

4)假设 这里的代码假设编译器没有插入代码来改变RSP。在某些情况下,这可能不正确,因此请注意不要做出这种假设。

5) 异常处理Win64 中的异常处理有点复杂,应该由编译器适当地完成(上面的示例代码没有这样做)。为了让编译器这样做,理想情况下,您的代码应该使用新的 BASM 指令/伪指令.PARAMS,正如Allen Bauer 在此处.PUSHNV所概述的那样。考虑到正确(错误)的情况,否则可能会发生坏事。.SAVENV

于 2012-02-27T08:53:02.800 回答
4

Delphi Win32 中的默认调用约定是“注册”。第一个参数在 EAX 中传递,第二个在 EDX 中传递,第三个在 ECX 中传递。仅当参数超过三个或传递大于 4 个字节的值类型时才使用堆栈,但在您的示例中并非如此。

您的第一个 CallObjMethWorking 过程有效,因为在调用 CallObjMethWorking 时编译器已将aStrValue放入 EDX 并将 aIntValue 放入 ECX。但是,由于您没有清理您的两个推送指令,所以当程序返回时,肯定会发生不好的事情。

您的代码应如下所示。在这种情况下,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;
于 2012-02-26T17:01:44.053 回答