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;

および次の2つの手順:

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;

そして最後に質問:CallObjMethWorkingが機能しているのに、なぜCallObjMethNOTWorking機能していないのですか?コンパイラがTMethodをどのように扱うかには何か特別なことがあると思いますが...アセンブリの知識が限られているため、理解できません。

誰かが私にこれを説明してくれれば幸いです、ありがとう!

4

2 に答える 2

6

HenrickHellströmの答えは正解です。あなたの質問はDelphi2010でタグ付けされているため、Win32のみに関係していることに気付きました。ただし、Win64(Delphi> = XE2)に進むと、状況がどのようになるかを確認したい場合があるため、Win64バージョンの例をHenrickのコードに追加しました。

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..endbegin..end32ビットのasmコードの周りにも影響があることに注意してください。具体的には、スタックフレームの生成を強制し、コンパイラに関数パラメータのローカルコピーを作成させます。(そもそもアセンブリを使用する場合は、コンパイラにそれを行わせたくない場合があります。)

2)呼び出し規約:Win64では、呼び出し規約は1つだけです。のようなものregisterstdcall事実上無意味です。それはすべて同じです、MicrosoftのWin64呼び出し規約。これは本質的にこれです:パラメータは、、、およびレジスタに渡されますRCX(およびRDX/または、の戻り値。64ビットより大きい値は参照を介して渡されます。R8R9XMM0-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。必要に応じて、呼び出された関数は、CPUを期待される状態に復元するために//命令をCLD発行する必要がありEMMSます。VZEROUPPER

3)配置とシャドウスペース 重要なことに、各関数にはスタック上に独自のシャドウスペースがあります。これは、パラメーターがなく、呼び出された関数が実際にそれに接触しているかどうかに関係なく、少なくとも4QWORDパラメーターに相当するスタックスペースです。さらに、各関数呼び出しのサイト(各CALLステートメント)では、RSP16バイトに整列されることが期待されます(ESPMacOSX32でも同じです)。これは、多くの場合、次のようなものにつながりますsub rsp, ##; call $$; add rsp, ##。##が、関数が呼び出される(QWORD)パラメーターの合計に加えて、のアラインメント用のオプションの8バイトである構成RSP。サイトRSPでのアラインメントは、関数の入力時に発生することに注意してください(CALLRSP = ###8hCALLRSPリターンアドレスをスタックに置きます)、したがって、あなたがする前に誰もいじらないと仮定すると、それがそれであると期待することができます。

提供されている例では、SSE2MOVDQA命令を使用してのアライメントをテストしていますRSP。(XMM5自由に変更できますが、関数パラメータデータを含めることができないため、デスティネーションレジスタとして使用されます)。

4)前提条件 ここでのコードは、コンパイラーが変更するコードを挿入しないことを前提としていますRSP。これが当てはまらない場合もあるので、この仮定をすることに注意してください。

5)例外処理Win64での例外処理は少し複雑であり、コンパイラによって適切に実行される必要があります(上記のサンプルコードはこれを実行しません)。コンパイラがそうできるようにするには、理想的には、コードで新しいBASMディレクティブ/疑似命令を使用する必要があります。これは、AllenBauerが.PARAMSここ.PUSHNV概説.SAVENVしているとおりです。正しい(間違った)状況を考えると、そうでなければ悪いことが起こる可能性があります。

于 2012-02-27T08:53:02.800 に答える
4

DelphiWin32のデフォルトの呼び出し規約は「レジスタ」です。最初のパラメーターはEAXで渡され、2番目はEDXで渡され、3番目はECXで渡されます。スタックは、3つを超えるパラメーターがある場合、または4バイトを超える値型が渡される場合にのみ使用されますが、この例ではそうではありません。

CallObjMethWorkingが呼び出されたときに、コンパイラがすでにaStrValueをEDXに配置し、aIntValueをECXに配置しているため、最初のCallObjMethWorkingプロシージャは機能します。ただし、2つのプッシュ命令をクリーンアップしていないため、プロシージャが戻ったときに悪いことが起こる可能性があります。

コードは次のようになります。この場合、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 に答える