dllからdllをロードするための最良の方法は何ですか?
私の問題は、process_attachにdllをロードできず、メインプログラムのソースを制御していないため、メインプログラムからdllをロードできないことです。したがって、dllmain以外の関数も呼び出すことができません。
dllからdllをロードするための最良の方法は何ですか?
私の問題は、process_attachにdllをロードできず、メインプログラムのソースを制御していないため、メインプログラムからdllをロードできないことです。したがって、dllmain以外の関数も呼び出すことができません。
コメントで行われたすべての議論の後、私は「本当の」答えに私の立場を要約する方が良いと思います。
まず第一に、LoadLibraryを使用してDllMainにdllをロードする必要がある理由はまだ明確ではありません。DllMainのドキュメントで説明されているように、ローダーロックを保持するLoadLibraryへの別の呼び出し内で、DllMainが実行されているため、これは間違いなく悪い考えです。
プロセスの初期起動中またはLoadLibraryの呼び出し後、システムはロードされたDLLのリストをスキャンしてプロセスを探します。DLL_PROCESS_ATTACH値でまだ呼び出されていないDLLごとに、システムはDLLのエントリポイント関数を呼び出します。この呼び出しは、プロセスのプライマリスレッドやLoadLibraryを呼び出したスレッドなど、プロセスのアドレス空間を変更したスレッドのコンテキストで行われます。エントリポイントへのアクセスは、プロセス全体でシステムによってシリアル化されます。DllMainのスレッドはローダーロックを保持しているため、追加のDLLを動的にロードまたは初期化することはできません。
エントリポイント関数は、単純な初期化または終了タスクのみを実行する必要があります。LoadLibraryまたはLoadLibraryEx関数(またはこれらの関数を呼び出す関数)を呼び出さないでください。これにより、DLLのロード順序に依存関係ループが作成される可能性があります。これにより、システムが初期化コードを実行する前にDLLが使用される可能性があります。同様に、エントリポイント関数は、プロセスの終了時にFreeLibrary関数(またはFreeLibraryを呼び出す関数)を呼び出さないでください。これにより、システムが終了コードを実行した後にDLLが使用される可能性があります。(強調を追加)
だから、これはなぜそれが禁止されているのかについてです。明確でより詳細な説明については、これとこれを参照してください。DllMainでこれらのルールに固執しない場合に何が起こるかについての他の例については、RaymondChenのブログのいくつかの投稿も参照してください。
さて、ラキスの答えです。
すでに数回繰り返したように、DllMainと思われるものは、dllの実際のDllMainではありません。代わりに、dllの実際のエントリポイントによって呼び出される関数です。これは、CRTによって自動的に実行され、追加の初期化/クリーンアップタスクを実行します。その中には、グローバルオブジェクトとクラスの静的フィールドの構築があります(実際、コンパイラの観点からは、これらはほとんど同じです)。もの)。そのようなタスクを完了した後(またはクリーンアップの前)に、DllMainを呼び出します。
それはどういうわけかこのようになります(明らかに私はすべてのエラーチェックロジックを書いたわけではありません、それはそれがどのように機能するかを示すためだけです):
/* This is actually the function that the linker marks as entrypoint for the dll */
BOOL WINAPI CRTDllMain(
__in HINSTANCE hinstDLL,
__in DWORD fdwReason,
__in LPVOID lpvReserved
)
{
BOOL ret=FALSE;
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
/* Init the global CRT structures */
init_CRT();
/* Construct global objects and static fields */
construct_globals();
/* Call user-supplied DllMain and get from it the return code */
ret = DllMain(hinstDLL, fdwReason, lpvReserved);
break;
case DLL_PROCESS_DETACH:
/* Call user-supplied DllMain and get from it the return code */
ret = DllMain(hinstDLL, fdwReason, lpvReserved);
/* Destruct global objects and static fields */
destruct_globals();
/* Destruct the global CRT structures */
cleanup_CRT();
break;
case DLL_THREAD_ATTACH:
/* Init the CRT thread-local structures */
init_TLS_CRT();
/* The same as before, but for thread-local objects */
construct_TLS_globals();
/* Call user-supplied DllMain and get from it the return code */
ret = DllMain(hinstDLL, fdwReason, lpvReserved);
break;
case DLL_THREAD_DETACH:
/* Call user-supplied DllMain and get from it the return code */
ret = DllMain(hinstDLL, fdwReason, lpvReserved);
/* Destruct thread-local objects and static fields */
destruct_TLS_globals();
/* Destruct the thread-local CRT structures */
cleanup_TLS_CRT();
break;
default:
/* ?!? */
/* Call user-supplied DllMain and get from it the return code */
ret = DllMain(hinstDLL, fdwReason, lpvReserved);
}
return ret;
}
これについて特別なことは何もありません。これは通常の実行可能ファイルでも発生し、メインは実際のエントリポイントによって呼び出されます。実際のエントリポイントは、まったく同じ目的でCRTによって予約されています。
これから、Rakisのソリューションが機能しない理由が明らかになります。グローバルオブジェクトのコンストラクターは、実際のDllMain(つまり、DllMainのMSDNページに関するものであるdllの実際のエントリポイント)によって呼び出されます。について話します)、したがって、そこからLoadLibraryを呼び出すことは、偽のDllMainから呼び出すこととまったく同じ効果があります。
したがって、そのアドバイスに従うと、DllMainでLoadLibraryを直接呼び出すのと同じ悪影響が得られます。また、一見無関係な位置に問題を隠すことができます。これにより、次のメンテナがこのバグの場所を見つけるのに苦労します。位置した。
delayloadについて:それはアイデアかもしれませんが、DllMainで参照されているdllの関数を呼び出さないように注意する必要があります。実際、これを行うと、LoadLibraryへの非表示の呼び出しがトリガーされます。直接呼び出すことの悪影響。
とにかく、私の意見では、dll内のいくつかの関数を参照する必要がある場合、最良のオプションはインポートライブラリに対して静的にリンクすることです。そのため、ローダーは問題なく自動的にロードし、奇妙な依存関係を自動的に解決します。発生する可能性のあるチェーン。
この場合でも、DllMainでこのdllの関数を呼び出さないでください。これは、既にロードされていることが保証されていないためです。実際、DllMainでは、ロードされているkernel32のみに依存できます。おそらく、dllをロードしているLoadLibraryが発行される前に、呼び出し元がすでにロードされていることを絶対に確信しています(ただし、これに依存するべきではありません。 dllは、これらの仮定に一致しないアプリケーションによってロードされる場合もあります。たとえば、コードを呼び出さずにdllのリソースをロードしたい場合があります)。
以前リンクした記事で指摘されているように、
重要なのは、バイナリに関する限り、DllMainは本当にユニークな瞬間に呼び出されるということです。その時までに、OSローダーはディスクからファイルを見つけ、マップし、バインドしましたが、状況によっては、ある意味で、バイナリが「完全に生まれた」わけではない可能性があります。物事はトリッキーになる可能性があります。
一言で言えば、DllMainが呼び出されると、OSローダーはかなり壊れやすい状態になります。まず、その呼び出し内での内部破損を防ぐために構造にロックを適用しました。次に、一部の依存関係が完全にロードされた状態になっていない可能性があります。バイナリがロードされる前に、OSローダーはその静的依存関係を調べます。それらに追加の依存関係が必要な場合は、それらも調べます。この分析の結果、これらのバイナリのDllMainsを呼び出す必要があるシーケンスが作成されます。物事についてはかなり賢く、ほとんどの場合、MSDNで説明されているほとんどのルールに従わなくても問題を解決できますが、常にそうとは限りません。
重要なのは、ロードの順序は不明ですが、さらに重要なのは、静的なインポート情報に基づいて構築されていることです。DLL_PROCESS_ATTACH中にDllMainで動的ロードが発生し、アウトバウンドコールを行っている場合、すべてのベットはオフになっています。そのバイナリのDllMainが呼び出される保証はありません。したがって、そのバイナリ内の関数にGetProcAddressを入れようとすると、グローバル変数が初期化されていない可能性があるため、結果は完全に予測できません。ほとんどの場合、AVを取得します。
(ここでも、強調が追加されました)
ちなみに、LinuxとWindowsの質問についてですが、私はLinuxシステムプログラミングの専門家ではありませんが、この点でそれほど違いはないと思います。
DllMain( _init関数と_fini関数)に相当するものがまだいくつかありますが、これは偶然の一致です。--CRTによって自動的に取得され、CRTは、_initから、グローバルオブジェクトのすべてのコンストラクターと__attribute__コンストラクターでマークされた関数(Win32でプログラマーに提供される「偽の」DllMainと同等)を呼び出します。同様のプロセスが_finiのデストラクタでも進行します。
dllのロードがまだ行われている間に_initも呼び出されるため(dlopenはまだ返されていません)、そこでできることについても同様の制限が課せられていると思います。それでも、Linuxについての私の意見では、(1)DllMainのような関数を明示的にオプトインする必要があるため、すぐに悪用したくなることはなく、(2)Linuxアプリケーション、私が見た限りでは、dllの動的ロードをあまり使用しない傾向があります。
したがって、DllMainから重要なことを何もしないでください。直接(つまり、CRTによって呼び出される「あなたの」DllMainで)、間接的に(グローバルクラス/静的フィールドコンストラクタで)、特に 他のdllをロードしないでください。 LoadLibrary経由)間接的にも(LoadLibrary呼び出しをトリガーする遅延ロードされたdll内の関数の呼び出しを使用して)。
依存関係として別のdllをロードする正しい方法は、-doh!-静的な依存関係としてマークします。静的インポートライブラリに対してリンクし、その関数の少なくとも1つを参照するだけです。リンカはそれを実行可能イメージの依存関係テーブルに追加し、ローダーはそれを自動的にロードします(DllMainの呼び出しの前後に初期化します。 DllMainから呼び出してはいけないので、それについて知る必要はありません)。
これが何らかの理由で実行可能でない場合でも、delayloadオプションがあります(前に述べた制限があります)。
それでも、何らかの理由で、DllMainのLoadLibraryを呼び出す必要がある場合は、先に進んで、足で撃ってください。それはあなたの学部にあります。しかし、私があなたに警告しなかったと私に言わないでください。
いいえ、私の質問に対する答えではありません。それが言うのは、「ダイナミックリンクでは不可能です。静的にリンクする必要があります」と「dllmainから呼び出す必要はありません」です。これはあなたの質問に対する答えです:あなたが課した条件の下では、あなたはあなたが望むことをすることができません。一言で言えば、DllMainから*kernel32関数以外*を呼び出すことはできません。限目。
詳細ではありますが、なぜそれが機能しないのかについてはあまり興味がありませんが、代わりに、ルールがそのように作成されている理由を理解することで、大きな間違いを回避できるためです。
実際、ローダーは依存関係を正しく解決しておらず、読み込みプロセスはMicrosoftの部分から不適切にスレッド化されています。いいえ、私の愛する人、ローダーは正しくその仕事をします。なぜなら、LoadLibraryが戻った後、すべての依存関係がロードされ、すべてを使用する準備ができているからです。ローダーは依存関係の順序でDllMainを呼び出そうとします(DllMain内の他のdllに依存する壊れたdllの問題を回避するため)が、これが単に不可能な場合があります。
たとえば、相互に依存する2つのdll(たとえば、A.dllとB.dll)が存在する場合があります。現在、DllMainが最初に呼び出すのは誰ですか。ローダーが最初にA.dllを初期化し、これがDllMainでB.dllの関数を呼び出した場合、B.dllはまだ初期化されていないため(DllMainはまだ呼び出されていません)、何かが発生する可能性があります。状況を逆転させても同じことが言えます。
同様の問題が発生する可能性がある他の場合もあるため、単純なルールは次のとおりです。DllMainで外部関数を呼び出さないでください。DllMainはdllの内部状態を初期化するためだけのものです。
問題は、dll_attachでそれを実行する他の方法がないことです。少なくとも私の場合は、代替手段がないため、何も実行しないことについてのすべての良い話は不要です。
この議論は次のように進行しています。「実領域でx^2 + 1=0のような方程式を解きたい」とあなたは言います。誰もがそれは不可能だとあなたに言います。あなたはそれが答えではないと言い、数学を非難します。
誰かがあなたに言います:ねえ、あなたはそうすることができます、ここにトリックがあります、解決策はちょうど+/- sqrt(-1)です。誰もがこの答えに反対票を投じます(あなたの質問は間違っているので、私たちは実際の領域の外に出ます)、そしてあなたは誰が反対票を投じたかを非難します。あなたの質問によると、なぜその解決策が正しくないのか、そしてなぜこの問題が実際の領域で解決できないのかを説明します。あなたは、なぜそれができないのか気にしない、あなたはそれを実際の領域でしかできないと言い、再び数学を非難します。
さて、何百万回も説明され、言い直されたように、あなたの答えは解決策がないので、なぜあなたはDllMainにdllをロードするようなばかげたことを「しなければならない」のか説明できますか?多くの場合、「不可能な」問題は、別の問題を解決するために奇妙なルートを選択したために発生し、デッドロックに陥ります。全体像を説明した場合、DllMainにdllをロードする必要のないより良い解決策を提案できます。
PS:DLL2(ole32.dll、Vista x64)をDLL1(mydll)に対して静的にリンクする場合、古いオペレーティングシステムでリンカーが必要とするdllのバージョンはどれですか?存在するもの(明らかに、32ビット用にコンパイルしていると仮定しています)。アプリケーションに必要なエクスポートされた関数が見つかったdllに存在しない場合、dllは単にロードされません(LoadLibraryは失敗します)。
知りたい場合はCreateRemoteThreadを使用して、インジェクションでポジティブになります。LinuxとMacでのみ、dll/sharedライブラリがローダーによってロードされます。dllを静的な依存関係(最初から提案されているもの)として追加すると、Linux / Macとまったく同じようにローダーによってロードされますが、説明したように、DllMainではまだ信頼できないため、問題は依然として存在します。 kernel32.dll以外のもの(ローダーが最初に依存関係を初期化するのに十分インテリジェントである場合でも)。
それでも、問題は解決されます。CreateRemoteThreadを使用してスレッド(実際にLoadLibraryを呼び出してdllをロードする)を作成します。DllMainでは、IPCメソッド(たとえば、名前付き共有メモリ、そのハンドルはinit関数で閉じるためにどこかに保存されます)を使用して、dllが提供する「実際の」init関数のアドレスをインジェクタープログラムに渡します。その後、DllMainは何もせずに終了します。代わりに、インジェクターアプリケーションは、CreateRemoteThreadによって提供されるハンドルを使用して、WaitForSingleObjectでリモートスレッドの終了を待機します。次に、リモートスレッドが終了した後(したがって、LoadLibraryが完了し、すべての依存関係が初期化されます)、インジェクターは、DllMainによって作成された名前付き共有メモリからリモートプロセスのinit関数のアドレスを読み取り、開始します。 CreateRemoteThreadを使用します。
問題:Windows 2000では、DllMainの名前付きオブジェクトを使用することは禁止されています。
Windows 2000では、名前付きオブジェクトはターミナルサービスDLLによって提供されます。このDLLが初期化されていない場合、DLLを呼び出すと、プロセスがクラッシュする可能性があります。したがって、このアドレスは別の方法で渡す必要がある場合があります。非常にクリーンな解決策は、dllに共有データセグメントを作成し、それをインジェクターアプリケーションとターゲットアプリケーションの両方にロードして、そのようなデータセグメントに必要なアドレスを配置することです。dllは明らかに最初にインジェクターにロードされ、次にターゲットにロードされる必要があります。そうしないと、「正しい」アドレスが上書きされるためです。
実行できるもう1つの非常に興味深い方法は、LoadLibraryを呼び出してinit関数のアドレスを返す小さな関数(アセンブリで直接)を他のプロセスメモリに書き込むことです。そこに書き込んだので、どこにあるかがわかっているので、CreateRemoteThreadを使用して呼び出すこともできます。
私の意見では、これは最良のアプローチであり、この素晴らしい記事に書かれているコードがすでに存在するため、最も簡単でもあります。それを見てください、それは非常に興味深いです、そしてそれはおそらくあなたの問題のためのトリックをするでしょう。
最も堅牢な方法は、最初のDLLを2番目のインポートライブラリにリンクすることです。このように、2番目のDLLの実際のロードは、Windows自体によって行われます。非常に些細なことのように聞こえますが、DLLが他のDLLに対してリンクできることを誰もが知っているわけではありません。Windowsは循環依存を処理することもできます。A.DLLがA.DLLを必要とするB.DLLをロードする場合、B.DLLのインポートは、A.DLLを再度ロードせずに解決されます。
遅延ロードメカニズムを使用することをお勧めします。DLLは、インポートされた関数を呼び出す最初の時点でロードされます。さらに、ロード機能とエラー処理を変更できます。詳細については、遅延ロードされたDLLのリンカサポートを参照してください。
考えられる答えの1つは、LoadLibraryとGetProcAddressを使用して、ロードされたdll内で見つかった/配置された関数へのポインターにアクセスすることです。ただし、これが適切な答えであるかどうかを判断するには、意図/ニーズが十分に明確ではありません。