5

私が制御できないホストによって呼び出される dll に、いくつかのプラグイン コードを書いています。

ホストは、プラグインが __stdcall 関数としてエクスポートされると想定します。ホストは、関数の名前と、それが期待する引数の詳細を通知され、LoadLibrary、GetProcAddress を介して動的に呼び出しを作成し、手動で引数をスタックにプッシュします。

通常、プラグイン dll は一定のインターフェイスを公開します。私のプラグインは、dll の読み込み時に設定されるインターフェイスを公開します。これを実現するために、私のプラグインは、dll のコンパイル時に定義された一連の標準エントリ ポイントを公開し、公開されている内部機能に必要に応じてそれらを割り当てます。

内部関数はそれぞれ異なる引数を取る場合がありますが、これは物理エントリポイント名とともにホストに伝えられます。私の物理的な dll エントリポイントはすべて単一の void * ポインターを取るように定義されており、最初の引数からのオフセットと、ホストに伝達された既知の引数リストを使用して、スタックから後続のパラメーターを自分でマーシャリングします。

ホストは正しい引数を使用してプラグインの関数を正常に呼び出すことができ、すべて正常に動作します...ただし、a) 私の関数は、定義されているはずのようにスタックをクリーンアップしていません。 __stdcall 関数は 4 バイトのポインターを受け取るため、呼び出し元がさらに多くの引数をスタックにプッシュした場合でも、常に最後に 'ret 4' を実行します。b) 戻り時に ret 4 が 4 バイトをスタックからあまりにも多くポップするため、引数を取らない関数を処理できません。

私のプラグインからホストの呼び出しコードまでたどってみると、実際には a) はそれほど大したことではないことがわかります。ホストは、ディスパッチ呼び出しから戻るまでスタックスペースをいくらか失います。その時点で、スタックフレームがクリーンアップされ、ゴミがクリーンアップされます。でも...

b) は __cdecl に切り替えて、まったくクリーンアップしないことで解決できます。a) ネイキッド関数に切り替えて、独自のジェネリック引数クリーンアップ コードを記述することで、a) を解決できると思います。

呼び出されたばかりの関数が使用する引数スペースの量を知っているので、次のように単純になることを望んでいました。

extern "C" __declspec(naked) __declspec(dllexport) void  * __stdcall EntryPoint(void *pArg1)
{                                                                                                        
   size_t argumentSpaceUsed;
   {
      void *pX = RealEntryPoint(
         reinterpret_cast<ULONG_PTR>(&pArg1), 
         argumentSpaceUsed);

      __asm
      {
         mov eax, dword ptr pX
      }
   }
   __asm
   {
      ret argumentSpaceUsed
   }
}

しかし、 ret にはコンパイル時定数が必要なため、それは機能しません...何か提案はありますか?

更新しました:

ロブ・ケネディの提案のおかげで、うまくいくようです...

extern "C" __declspec(naked) __declspec(dllexport) void  * __stdcall EntryPoint(void *pArg1)
{      
   __asm {                                                                                                        
      push ebp          // Set up our stack frame            
      mov ebp, esp  
      mov eax, 0x0      // Space for called func to return arg space used, init to 0            
      push eax          // Set up stack for call to real Entry point
      push esp
      lea eax, pArg1                
      push eax                      
      call RealEntryPoint   // result is left in eax, we leave it there for our caller....         
      pop ecx 
      mov esp,ebp       // remove our stack frame
      pop ebp  
      pop edx           // return address off
      add esp, ecx      // remove 'x' bytes of caller args
      push edx          // return address back on                   
      ret                        
   }
}

これは正しく見えますか?

4

1 に答える 1

4

ret一定の引数が必要なので、関数が一定数のパラメーターを持つように調整する必要がありますが、その状況は、関数から戻る準備ができた時点でのみ必要です。したがって、関数の終了直前に、次のようにします。

  1. リターン アドレスをスタックの一番上からポップし、一時的に格納します。ECXは良い場所です。
  2. 各引数を個別にポップするか、ESP直接調整することにより、可変数の引数をスタックから削除します。
  3. 戻りアドレスをスタックにプッシュします。
  4. ret定数引数とともに使用します。

ちなみに、あなたが(a)と呼んでいる問題は、一般的なケースでは本当に問題です。幸運なことに、呼び出し元が常にスタック ポインターの代わりにフレーム ポインターを使用して独自のローカル変数を参照しているようです。ただし、関数はそれを行う必要はなく、ホスト プログラムの将来のバージョンが引き続きそのように動作するという保証はありません。コンパイラは、呼び出しの間だけスタックにいくつかのレジスタ値を保存する傾向があり、後でそれらを再びポップできることを期待しています。あなたのコードはそれを壊します。

于 2009-06-30T18:51:55.540 に答える