Visual Studio でカスタム プロローグ コードとエピローグ コードを使用してネイキッド関数を作成する
-
23-08-2019 - |
質問
私は制御できないホストによって呼び出される dll にプラグイン コードを作成しています。
ホストは、プラグインが __stdcall 関数としてエクスポートされると想定します。ホストには、関数の名前と、期待される引数の詳細が通知され、LoadLibrary、GetProcAddress を介してその呼び出しを動的に作成し、手動で引数をスタックにプッシュします。
通常、プラグイン DLL は定数インターフェイスを公開します。私のプラグインは、DLL のロード時に設定されるインターフェイスを公開します。これを実現するために、私のプラグインは、DLL のコンパイル時に定義される一連の標準エントリ ポイントを公開し、必要に応じて公開されている内部機能にそれらを割り当てます。
各内部関数は異なる引数を取ることができますが、これは物理エントリポイント名とともにホストに伝達されます。すべての物理 DLL エントリポイントは、単一の void * ポインターを受け取るように定義されており、最初の引数とホストに通知された既知の引数リストからのオフセットに基づいて、スタックから後続のパラメーターを自分でマーシャリングします。
ホストは正しい引数を指定してプラグインの関数を正常に呼び出すことができ、すべて正常に動作します...ただし、a) 私の関数は、4バイトのポインターを取る__stdcall関数として定義されているため、スタックをクリーンアップしていないことを認識しています。そのため、関数は常に「ret 4」を実行します。呼び出し元がさらに多くの引数をスタックにプッシュした場合でも終了します。b) ret 4 は戻り時にスタックから 4 バイトをポップしすぎるため、引数を取らない関数を処理できません。
プラグインからホストの呼び出しコードをトレースすると、実際には a) はそれほど大したことではないことがわかります。ホストはディスパッチ呼び出しから戻るまでスタックスペースを失い、その時点でスタックフレームをクリーンアップしてゴミをクリーンアップします。しかし...
b) は __cdecl に切り替え、クリーンアップをまったく行わないことで解決できます。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
}
}
これは正しいように見えますか?
解決
以来 ret
定数の引数が必要な場合は、関数が一定数のパラメータを持つように調整する必要がありますが、その状況が必要になるのは、関数から戻る準備ができた時点でのみです。したがって、関数の終了直前にこれを実行します。
- 戻りアドレスをスタックの最上位からポップし、一時アドレスに格納します。
ECX
良いところです。 - 可変数の引数をスタックから削除するには、各引数を個別にポップするか調整します。
ESP
直接。 - 戻りアドレスをスタックにプッシュして戻します。
- 使用
ret
定数引数を使用します。
ちなみに、あなたが (a) として言及している問題は、一般的なケースでは実際に問題です。呼び出し側が常にスタック ポインタではなくフレーム ポインタを使用して独自のローカル変数を参照しているように見えるのは幸運でした。ただし、関数がそれを行う必要はなく、ホスト プログラムの将来のバージョンがそのように動作し続けるという保証はありません。また、コンパイラは、呼び出しの間だけ一部のレジスタ値をスタックに保存し、その後それらの値を再びポップできることを期待する傾向があります。コードを作成するとそれが壊れてしまいます。