この記事は、インテル® デベロッパー・ゾーンに公開されている「The Ultimate Question of Programming, Refactoring, and Everything」の日本語参考訳です。
5. 利用可能なツールを使用してコードを解析する
LibreOffice プロジェクトから抜粋した以下のコードについて考えてみます。このエラーは、次の PVS-Studio 診断によって検出されます。
V718 The ‘CreateThread’ function should not be called from ‘DllMain’ function. (V718 ‘CreateThread’ 関数は、’DllMain’ 関数から呼び出されるべきではありません。)
BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved ) { .... CreateThread( NULL, 0, ParentMonitorThreadProc, (LPVOID)dwParentProcessId, 0, &dwThreadId ); .... }
説明
私は、以前副業としてフリーランサーをしていたことがあります。そのとき、一度だけ与えられたタスクを達成できないことがありました。タスク自体に問題があったのですが、当時はそのことに気付きませんでした。当初、そのタスクは明確で単純なものに思われました。
DllMain において特定の状況下で、Windows* API 関数を使用して処理を行う必要がありました。どのような処理かは覚えていませんが、難しい処理ではありませんでした。
長い時間を費やしましたが、コードはうまく動作しませんでした。その上、新しく作成した標準アプリケーションでは動作し、DllMain 関数では動作しなかったのです。不可解でしょう? 当時は、問題の原因を突き止めることができませんでした。
数年後 PVS-Studio の開発に携わるようになってから、その原因が分かりました。DllMain 関数では、非常に限られた処理のみ行うことができます。DLL がロードされていない場合、それらから関数を呼び出すことはできません。
現在では、DllMain 関数で検出された危険な操作をプログラマーに警告する診断があります。つまり、これが原因だったのです。
詳細
DllMain の使用に関する詳細は、MSDN* サイトの記事「ダイナミック・リンク・ライブラリーのベスト・プラクティス」 (英語) を参照してください。ここでは、いくつかを抜粋して紹介します。
DllMain は、ローダーロックが保持されている間に呼び出されます。そのため、DllMain 内で呼び出すことができる関数は大幅に制限されます。そのような理由から、DllMain は、Microsoft* Windows* API の小さなサブセットを使用して、最小限の初期化タスクを実行するように設計されています。直接または間接的にローダーロックを取得しようとする関数は、DllMain から呼び出すことができません。そうでないと、アプリケーションでデッドロックやクラッシュが発生する可能性があります。DllMain 実装のエラーは、プロセス全体とそのすべてのスレッドに影響します。
理想的な DllMain は、空のスタブです。しかし、多くのアプリケーションの複雑さを考慮すると、一般にこれは厳しすぎます。通常、DllMain では初期化をできるだけ延期したほうが良いでしょう。ローダーロックを保持している間はこの初期化が行われないため、初期化を遅らせることでアプリケーションの堅牢性が高まります。また、より多くの Windows* API を安全に使用できます。
いくつかの初期化タスクは延期できません。例えば、設定ファイルに依存する DLL は、ファイルが不正な形式であったり、ファイルに不要なデータが含まれている場合、ロードに失敗します。このような初期化では、DLL は処理の実行を試み、失敗した場合は他の作業にリソースを費やさずに直ちに終了すべきです。
次のタスクは、DllMain 内から実行すべきではありません。
- LoadLibrary または LoadLibraryEx の (直接または間接) 呼び出し。これは、デッドロックまたはクラッシュを引き起こします。
- GetStringTypeA、GetStringTypeEx、または GetStringTypeW の (直接または間接) 呼び出し。これは、デッドロックまたはクラッシュを引き起こします。
- ほかのスレッドとの同期。これは、デッドロックを引き起こします。
- ローダーロックの取得を待機しているコードが所有する同期オブジェクトの取得。これは、デッドロックを引き起こします。
- CoInitializeEx を使用した COM スレッドの初期化。この関数は、特定の状況下で LoadLibraryEx を呼び出すことができます。
- レジストリー関数の呼び出し。これらの関数は、Advapi32.dll で実装されています。DLL よりも前に Advapi32.dll が初期化されていないと、DLL が初期化されていないメモリーにアクセスしてプロセスがクラッシュする場合があります。
- CreateProcess の呼び出し。プロセスの作成は、別の DLL をロードする可能性があります。
- ExitThread の呼び出し。DLL デタッチ中にスレッドを終了すると、ローダーロックが再度取得され、デッドロックやクラッシュを引き起こす可能性があります。
- CreateThread の呼び出し。スレッドの作成は、ほかのスレッドと同期しなければ問題ありませんが、危険です。
- 名前付きパイプやほかの名前付きオブジェクトの作成 (Windows* 2000 のみ)。Windows* 2000 では、名前付きオブジェクトはターミナルサービス DLL によって提供されます。この DLL が初期化されていない場合、DLL 呼び出しはプロセスのクラッシュを引き起こします。
- ダイナミック C ランタイム (CRT) からのメモリー管理関数の使用。CRT DLL が初期化されていない場合、これらの関数呼び出しはプロセスのクラッシュを引き起こします。
- User32.dll または Gdi32.dll での関数呼び出し。一部の関数は別の DLL をロードしますが、その DLL が初期化されていない可能性があります。
- マネージドコードの使用。
正しいコード
上記の LibreOffice プロジェクトから抜粋したコードは、動作することも、しないこともあります。すべては運次第です。
このようなエラーを修正するのは容易ではありません。DllMain 関数をできるだけ単純かつ簡潔にするため、コードのリファクタリングが必要になります。
推奨事項
このエラーに対する推奨を示すことは困難です。誰もすべてを知ることはできません。誰もがこのような不可解なエラーに直面します。形式的な推奨は、作業するすべてのプログラム・エンティティーのすべてのドキュメントを注意深く読むべきであるといったものになるでしょう。しかし、ご存知のとおり、すべての潜在的な問題を予測することはできません。ドキュメントを読むことに時間を費やしたら、プログラムする時間がなくなってしまいます。そして、多くのページを読んだとしても、問題に対する警告の記載を見落とさなかったという確証は得られません。
皆さんに実用的なヒントを提示できれば良いのですが、残念ながら、私が考えられることは 1 つだけです。静的アナライザーを使用することです。それでも、エラーがゼロになる保証はありません。何年も前にアナライザーがあったなら、DllMain では Foo 関数を呼び出せないことを教えてくれたでしょう。そうすれば、私は時間を無駄にしたり、タスクを解決できないため、非常に腹立たしく気が滅入るほど神経をすり減らさずに済みました。
コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。