18


14

私はAllen Bauerの一般的なマルチキャストイベントディスパッチャのコードを調べていました(それについての彼のブログ記事 hereを参照)。

彼は私がそれを使いたくなるのにちょうど十分なコードを与えています、そして残念ながら彼は完全なソースを投稿していません。 私はそれを機能させることにはちょっと苦労しましたが、私のアセンブラスキルは存在しません。

私の問題はInternalSetDispatcherメソッドです。 単純な方法は、他のInternalXXXメソッドと同じアセンブラを使用することです。

プロシージャInternalSetDispatcher。 XCHG EAXを開始、[ESP] POP EAX POP EBP JMP SetEventDispatcher終了。

しかしこれは次のように一つのconstパラメータを持つ手続きに使われます。

手続きAdd(const AMethod:T);過負荷

そしてSetDispatcherには2つのパラメータがあり、1つはvarです。

プロシージャーSetEventDispatcher(var ADispatcher:T; ATypeData:PTypeData);

だから、私はスタックが破損するだろうと思います。 コードが何をしているのか(隠し参照をselfにポップすることによってInternalSetDispatcherへの呼び出しからスタックフレームをクリーンアップし、戻りアドレスを想定する)、全体を取得するためのアセンブラのちょっとした部分がわからない物事は行きます。

編集:ちょうど明確にするために、私が探しているのは、InternalSetDispatcherメソッドを機能させるために使用できるアセンブラ、つまり2つのパラメータを持つプロシージャのスタックをクリーンアップするためのアセンブラです。

編集2:私は質問を少し修正しました。これまでの回答に対してMasonに感謝します。 上記のコードは機能しないこと、そしてSetEventDispatcherが戻ったときにAVが発生することに注意してください。

2 Answer


15


答えは、Web上で何度も実行した後、InternalSetDispatcherを呼び出すときにスタックフレームが存在するとアセンブラが判断することです。

InternalSetDispatcherの呼び出しに対してスタックフレームが生成されていないようです。

ですから、修正は\ {$ stackframes on}コンパイラ指令でスタックフレームを有効にして再構築するのと同じくらい簡単です。

私がこの答えにたどり着くのにあなたの助けをありがとうメイソン。 :)

'' '' '

*編集2012-08-08 *:これを使いたければ、https://bitbucket.org/sglienke/spring4d/src/1b56ec987ef09c672b6acc381ca9cdf4ec1a46e1/Source/Base/Springの実装をチェックしてください。 pas [Delphi Sping Framework]。 まだテストしていませんが、このコードよりもさまざまな呼び出し規約を処理できるようです。

'' '' '

*編集:*要求されたように、アランのコードの私の解釈は以下の通りです。 スタックフレームを有効にする必要があることに加えて、これを機能させるには、プロジェクトレベルで*最適化を有効にする必要があります。

ユニットMulticastEvent。

インタフェース

クラス、SysUtils、Generics.Collections、ObjAuto、TypInfoを使用します。

タイプ

//これを機能させるには、プロジェクトオプションで//最適化を有効にしておく必要があります。 理由がわからない。 {$ stackframes on} {$ ifopt O-} {$ messageこのユニットを動作させるには、最適化をオンにする必要があります。 '} {$ endif} TMulticastEvent = class strict protected type TEvent =オブジェクトの手続き。厳格なプライベートFハンドラー:TList。 FINternalDispatcher:TMethod;

プロシージャInternalInvoke(Params:PParameters; StackSize:Integer);プロシージャSetDispatcher(var AMethod:TMethod; ATypeData:PTypeData); procedure Add(const AMethod:TEvent);過負荷procedure Remove(const AMethod:TEvent);過負荷関数IndexOf(const AMethod:TEvent):整数。過負荷保護されたプロシージャInternalAdd。プロシージャInternalRemove。プロシージャInternalIndexOf;プロシージャInternalSetDispatcher。

パブリックコンストラクタデストラクタオーバーライド;

終わり;

TMulticastEvent = class(TMulticastEvent)厳密なプライベートFInvoke:T;プロシージャーSetEventDispatcher(var ADispatcher:T; ATypeData:PTypeData);パブリックコンストラクタ手続きAdd(const AMethod:T);過負荷procedure Remove(const AMethod:T);過負荷function IndexOf(const AMethod:T):整数。過負荷

property Invoke:T読みます。終わり;

実装

{TMulticastEvent}

手順TMulticastEvent.Add(const AMethod:TEvent); FHandlers.Add(TMethod(AMethod))を終了します。

コンストラクタTMulticastEvent.Create;継承し始めます。 FHandlers:= TList.Create;終わり;

デストラクタTMulticastEvent.Destroy; ReleaseMethodPointer(FInternalDispatcher)を開始します。 FreeAndNil(Fハンドラ);継承されました;終わり;

関数TMulticastEvent.IndexOf(const AMethod:TEvent):整数。結果の開始:= FHandlers.IndexOf(TMethod(AMethod));終わり;

手順TMulticastEvent.InternalAdd; asm XCHG EAX、[ESP]ポップEAXポップEBP JMP追加の終わり。

手順TMulticastEvent.InternalIndexOf; asm XCHG EAX、[ESP]ポップEAXポップEBP JMP IndexOf;

procedure TMulticastEvent.InternalInvoke(Params:PParameters; StackSize:Integer); var LMethod:TMethod; FHandlerでLMethodを開始します。do begin //スタックに何かがあるかどうかを確認します。 StackSize> 0の場合、asm //スタックに項目がある場合は、そこにスペースを割り当て、//そのデータを移動します。 MOV ECX、スタックサイズSUB ESP、ECX MOV EDX、ESP MOV EAX、Params LEA EAX、[EAX] .TParameters.Stack [8] CALL System.Move end; asm //今度はレジスタをロードする必要があります。 EDXとECXはデータを持っているかもしれません。 MOV EAX、Params MOV EDX、[EAX] .TParameters.Registers.DWORD [0] MOV ECX、[EAX] .TParameters.Registers.DWORD [4] // EAXは常に "Self"であり、メソッドごとのポインタで変わるインスタンスなので、//メソッドデータから取得します。 MOV EAX、LMethod.Data //今度はメソッドを呼び出します。 これは、上記の操作を行った場合に、呼び出されたメソッドが//スタックをクリーンアップするという事実に依存します。 LMethod.Code endをCALLします。終わり;終わり;

TMulticastEvent.InternalRemoveプロシージャasm XCHG EAX、[ESP]ポップEAXポップEBP JMP

TMulticastEvent.InternalSetDispatcherプロシージャasm XCHG EAX、[ESP]ポップEAXポップEBP JMP SetDispatcher;終わり;

手順TMulticastEvent.Remove(const AMethod:TEvent); FHandlers.Remove(TMethod(AMethod))を開始します。終わり;

プロシージャTMulticastEvent.SetDispatcher(var AMethod:TMethod; ATypeData:PTypeData); Assigned(FInternalDispatcher.Code)およびAssigned(FInternalDispatcher.Data)の場合は、ReleaseMethodPointer(FInternalDispatcher)を指定します。 FInternalDispatcher:= CreateMethodPointer(InternalInvoke、ATypeData); AMethod:= FInternalDispatcher;終わり;

{TMulticastEvent}

手順TMulticastEvent.Add(const AMethod:T); InternalAddを開始します。終わり;

コンストラクタTMulticastEvent.Create; var MethInfo:PTypeInfo; TypeData:PTypeData。 MethInfo:= TypeInfo(T)を開始します。 TypeData:= GetTypeData(MethInfo);継承されたAssert(MethInfo.Kind = tkMethod、 'Tはメソッドポインタ型でなければならない'); SetEventDispatcher(FInvoke、TypeData);終わり;

関数TMulticastEvent.IndexOf(const AMethod:T):整数。 InternalIndexOfを開始します。終わり;

手順TMulticastEvent.Remove(const AMethod:T); InternalRemoveを開始します。終わり;

TMulticastEvent.SetEventDispatcher(var ADispatcher:T; ATypeData:PTypeData); InternalSetDispatcherを開始します。終わり;

終わり。


6


ブログ投稿から:

_ この関数は、自身と直接の呼び出し元を呼び出しチェーンから削除し、渡されたパラメーターを保持しながら、対応する「安全でない」メソッドに制御を直接転送します。 _

このコードは、InternalAddのスタックフレームを削除しています。InternalAddには、1つのパラメーター「+ Self +」しかありません。 それはあなたが渡したイベントには影響を与えないので、たった1つのパラメータと* register *呼び出し規約を持つ他の関数のためにコピーしても安全です。

*編集:*コメントに答えて、あなたが逃している点があります。 「コードが何をしているか(親呼び出しからスタックフレームをクリーンアップする)ことを知っています」と書いたとき、あなたは間違っていました。 *親の呼び出しには触れません*スタックのフレームをAddからクリーンアップするのではなく、スタックのフレームをCurrentからの呼び出しInternalAddからクリーンアップします。

あなたはこの点について少し混乱しているように思われるので、これは少し基本的なオブジェクト指向理論です。 Addは1つのパラメータを_実際には持たず、SetEventDispatcherは2つを持たない。 実際にはそれぞれ2つと3つあります。 * static *と宣言されていないメソッド呼び出しの最初のパラメーターは `+ Self +`であり、コンパイラーによって目に見えないように追加されます。 つまり、3つの内部関数はそれぞれ1つのパラメータを持ちます。 それを書いたとき、それは私が意味したことです。

Allenのコードがしていることは、コンパイラの制限を回避することです。 すべてのイベントはメソッドポインタですが、ジェネリックのための「メソッド制約」がないので、コンパイラはTが常にTMethodにキャストできる8バイトレコードになることを知りません。 (実際、そうである必要はありません。 プログラムを新しく興味深い方法で壊したい場合は、 `+ TMulticastEvent +`を作成できます。)内部メソッドはアセンブリを使用して、コールスタックから完全に自分自身を取り除き、JMP(基本的にはGOTO)適切なメソッド。呼び出し元の関数と同じパラメーターリストを残します。

だからあなたが見るとき

procedure TMulticastEvent.Add(const AMethod: T);
begin
  InternalAdd;
end;

それがコンパイルするならば、それがしていることは以下と同等です。

procedure TMulticastEvent.Add(const AMethod: T);
begin
  Add(TEvent(AMethod));
end;

あなたのInternalSetDispatcherは全く同じことをしたいでしょう:それ自身の1つのパラメータ呼び出しを取り除いて、そして呼び出しメソッド、SetEventDispatcherが持っていたのとまったく同じパラメータリストでSetDispatcherにジャンプしてください。 呼び出し元の関数のパラメータやジャンプ先の関数は関係ありません。 重要なのは(そしてこれは重要です!)、SetEventDispatcherとSetDispatcherは互いに同じ呼び出しシグニチャーを持っているということです。

つまり、あなたが投稿した仮想コードは問題なく動作し、コールスタックを破壊することはありません。