「特定の環境でだけ、アプリの起動時にハングする」「自作DLLを読み込むと、LoadLibrary から戻ってこないことがある」「サービス起動のタイミングでだけデッドロックする」── こうした調査を進めていくと、かなりの確率でたどり着く場所があります。DLLの初期化コード、つまり DllMain です。
DllMain について、Microsoftのドキュメントは異例なほど強い調子で警告しています。いわく、LoadLibraryを呼ぶな。他スレッドと同期するな。User・Shell・COMの関数を呼ぶな。理想の DllMain は空のスタブである ── なぜここまで言われるのか。その理由はひとつの内部機構、ローダーロック(loader lock)に集約されます。この記事では、WindowsでDLLやプラグイン、C++/CLIラッパーを書く開発者を対象に、ローダーロックの仕組みとデッドロックが成立する構造、そして安全な設計と調査の手順を一次情報にもとづいて解説します。
1. まず結論
DllMainはローダーロックという、プロセスごとに1本だけ存在する共有ロックを保持したまま呼ばれます。そのため、ローダーロックを(直接・間接に)取ろうとする処理をDllMainから呼ぶと、デッドロックや、初期化前のDLLに触れるクラッシュの可能性が生まれます。1LoadLibrary/FreeLibraryの呼び出しは禁止です。ロード順序の依存関係が循環し、初期化コードが実行される前のDLLが使われる事態を招きます。2- 他スレッドとの同期も禁止です。DLL通知は直列化されているため、
DllMainの中でスレッドの開始や終了を待つと、そのスレッド自身がローダーロック待ちで止まり、デッドロックします。23 - 安全に呼べるのは実質Kernel32.dllの一部だけです。しかも「安全な関数の完全な一覧は存在しない」と公式が明言しています。User・Shell・COM関数は他のコンポーネントをロードするためアクセス違反の原因になります。2
- CRTとリンクしたDLLでは、グローバル変数のコンストラクタ・デストラクタにも同じ制約がかかります。これらは実質
DllMainの一部として実行されるからです。2 - 正しい設計は「遅延」です。できる初期化はコンパイル時(静的)に、できないものは初回利用時に遅延する。これが公式のベストプラクティスです。1
- C++/CLIの混在DLLは特に危険です。ローダーロック下でMSILを実行しないよう、
DllMainとその呼び出し木はネイティブコンパイルが必須です。4
2. DllMainはいつ・どう呼ばれるのか
DllMain は、DLLがプロセスやスレッドに出入りするタイミングでOSのローダーから呼ばれるエントリポイントで、通知は4種類あります。
| 通知 | タイミング |
|---|---|
| DLL_PROCESS_ATTACH | DLLがプロセスにロードされたとき |
| DLL_THREAD_ATTACH | プロセス内で新しいスレッドが開始されたとき |
| DLL_THREAD_DETACH | スレッドが正常終了するとき |
| DLL_PROCESS_DETACH | DLLのアンロード時、またはプロセス終了時 |
見落とされがちな事実が2つあります。第一に、スレッドが1本作られるたびに、ロード済みのすべてのDLLの DllMain が DLL_THREAD_ATTACH で呼ばれること。つまり DllMain は「自分のDLLがロードされたときに1回だけ動くもの」ではなく、プロセスのスレッド活動のたびに呼ばれ続けるコードです。不要ならDLL_PROCESS_ATTACHの中で DisableThreadLibraryCalls を呼んで止められます(静的CRTとリンクしたDLLでは呼んではいけません)。5
第二に、CRT(C/C++ランタイム)とリンクしたDLLでは、グローバル・静的C++オブジェクトのコンストラクタとデストラクタが、CRTのエントリポイント経由で DllMain の一部として実行されることです。2 「うちの DllMain は空だから安全」と思っていても、凝った初期化をするグローバルオブジェクトがあれば、それは DllMain で実行しているのと同じです。
flowchart TB
accTitle: DllMainが呼ばれる4つのタイミング
accDescr: DLLのロードでDLL_PROCESS_ATTACHが、プロセス内のスレッド開始・終了のたびにロード済み全DLLへDLL_THREAD_ATTACHとDETACHが、アンロードやプロセス終了でDLL_PROCESS_DETACHが呼ばれ、CRT経由で静的オブジェクトのコンストラクタもこの中で実行される
load["DLLロード"] --> pa["DLL_PROCESS_ATTACH"]
pa --> ta["DLL_THREAD_ATTACH(スレッド開始のたび)"]
ta --> td["DLL_THREAD_DETACH(スレッド終了のたび)"]
td --> pd["DLL_PROCESS_DETACH(アンロード・終了時)"]
pa -.-> crt["静的オブジェクトの構築もここで実行"]
図1: DllMainはロード時だけでなくスレッドの開始・終了のたびに呼ばれ、静的オブジェクトの初期化もその一部として動く。
3. ローダーロック ── すべての通知を直列化する1本のロック
なぜ DllMain にだけ、これほど厳しい制約が課されるのか。答えはローダーの構造にあります。
OSのローダーは、DLLのロード・アンロード・各種通知という一連の作業の整合性を守るため、プロセスにただ1つのローダーロックで処理を直列化しています。そして重要なのは、DllMain はこのローダーロックを保持したまま呼ばれることです。1 DllMain の中にいる間、そのプロセスでは他のDLLのロードも、新しいスレッドの開始通知も、すべてこのロックの解放待ちになります。
この構造から、禁止事項の理由が芋づる式に導けます。
LoadLibraryを呼んではいけないのは、ローダーロックの再帰や、ロード順序の循環依存を作るからです。初期化がまだ済んでいないDLLの関数が呼ばれる事態も起きえます。2- 他スレッドとの同期が危険なのは、待っている相手のスレッドがローダーロックを必要とする瞬間(開始・終了時の通知、
GetModuleHandle系APIの呼び出しなど)があるからです。自分はローダーロックを持って相手を待ち、相手はローダーロックを待つ ── 古典的なロック順序逆転です。6 - User・Shell・COMの関数が危険なのは、これらが内部で他のシステムコンポーネントをロードするためです。初期化前・解放後のコンポーネントに触れてアクセス違反になります。2
sequenceDiagram
accTitle: DllMainでスレッドを待つとデッドロックする構造
accDescr: ローダーロックを保持したDllMainがワーカースレッドの終了を待つが、終了しようとするワーカースレッドはDLL_THREAD_DETACH通知のためにローダーロックの解放を待つため、互いに待ち合ってデッドロックになる
participant L as ローダー(ロック保持)
participant D as DllMain
participant W as ワーカースレッド
L->>D: DLL_PROCESS_DETACHを通知
D->>W: 終了を要求し完了を待つ
W->>W: 処理を終えてスレッド終了へ
Note over W: 終了通知にローダーロックが必要
Note over D,W: DllMainはロックを持って待ち、Wはロックを待つ
図2: 「DllMainがスレッドの終了を待つ」は、スレッド終了自体がローダーロックを必要とするため構造的にデッドロックする。
ポイントは、これが「運が悪いと起きる」類のものではなく、構造的に成立してしまうことです。ドキュメントはローダーロックを、アプリが定義するロック階層の最上位(最初に取られるもの)として扱えと指示しています。DllMain の中では自分は既にその最上位ロックを持っているのだから、そこからさらに何かを待ちに行く行為全般が危険になる、と整理すると覚えやすいでしょう。6
flowchart TB
accTitle: ローダーロックと私有ロックの順序逆転
accDescr: DllMainはローダーロックを持った状態で私有ロックを取りに行き、ワーカースレッドは私有ロックを持った状態でGetModuleHandleなどのためにローダーロックを取りに行くため、取得順序が逆転してデッドロックする
d["DllMain:ローダーロック保持中"] --> dg["私有ロックGを取りに行く"]
w["ワーカー:私有ロックG保持中"] --> wl["ローダーロックを取りに行く"]
dg -.-> dead["取得順序の逆転でデッドロック"]
wl -.-> dead
wl -.-> api["GetModuleHandle等が内部で要求"]
図3: GetModuleHandleのような何気ないAPIも内部でローダーロックを要求するため、私有ロックとの順序逆転が成立しうる。
なお、DllMain の中で CreateThread を呼ぶこと自体も推奨されません。作られたスレッドはDLL_THREAD_ATTACH通知の処理にローダーロックを必要とするため、いま実行中の DllMain が返ってロックを解放するまで実行を開始できません。したがって DllMain の中でそのスレッドの開始や完了を待てば、その場でデッドロックです。さらに寿命の問題もあります ── DllMain が返った後、まだ走り出していないスレッドを残したままDLLがアンロードされると、スレッドの開始番地が解放済みのコードを指したまま実行されてクラッシュします。3
4. C++開発者が踏みやすい2つの地雷
地雷1: グローバルオブジェクトの動的初期化。2章で述べたとおり、静的オブジェクトのコンストラクタは DllMain の制約下で走ります。設定ファイルを読む、ログ機構を立ち上げる、COMを初期化する、スレッドを起動する ── そうした仕事をコンストラクタに持つグローバル変数をDLLに置いた瞬間、それは「DllMain でやってはいけないこと」の実行になります。コンパイル時に決まる定数初期化(constexpr にできるもの)は安全ですが、関数呼び出しを伴う初期化は遅延させてください。
flowchart TB
accTitle: グローバルオブジェクトの初期化が地雷になる経路
accDescr: DLLのロードでローダーロックが取得され、CRT経由でグローバルオブジェクトのコンストラクタが実行されるため、その中のLoadLibraryやスレッド同期やCOM初期化はDllMainの禁止事項の実行になる
load["DLLロード(ローダーロック取得)"] --> crt["CRTのエントリポイント"]
crt --> ctor["グローバルオブジェクトのコンストラクタ"]
ctor --> ng1["LoadLibrary相当の処理"]
ctor --> ng2["スレッド起動と完了待ち"]
ctor --> ng3["COMやUser32の利用"]
ng1 -.-> risk["すべてDllMainの禁止事項に該当"]
ng2 -.-> risk
ng3 -.-> risk
図4: 「DllMainは空だから安全」でも、凝った初期化を持つグローバル変数があれば同じ危険が復活する。
地雷2: C++/CLI(混在アセンブリ)。ネイティブDLLをC++/CLIでラップする構成(ラッパー記事で扱った形)では、ローダーロック下でMSIL(マネージドコード)を実行してしまう危険があります。MSILの実行はCLRの初期化や他アセンブリのロードを引き起こしうるからです。コンパイラは DllMain が直接MSILを実行するコードには警告C4747を出しますが、別モジュールの関数を経由した間接的な実行は検出できません。DllMain とそこから呼ばれる関数は #pragma unmanaged でネイティブにコンパイルするか、DllMain 自体を持たない構成にします。4
flowchart TB
accTitle: ローダーロック下のMSIL実行の検出可否
accDescr: DllMainが直接MSILを実行するコードはコンパイラが警告C4747で検出できるが、別モジュールの関数を経由した間接的な実行は検出できないため、呼び出し木のレビューとネイティブコンパイルの徹底で防ぐ必要がある
d2["DllMainからの呼び出し"] --> dir["直接MSILを実行"]
d2 --> ind["別モジュール経由で実行"]
dir --> c47["警告C4747で検出できる"]
ind --> nc["コンパイラは検出できない"]
nc -.-> rv["レビューと#pragma unmanagedで防ぐ"]
図5: C4747が守ってくれるのは直接実行だけ。間接経路はレビューでしか拾えない。
5. 正しい設計 ── 「遅延」を基本方針にする
公式ベストプラクティスの推奨は明快です。1
- できる初期化はコンパイル時(静的)に済ませる。動的初期化を静的初期化に置き換えられないか、まず考える。
- 残りは初回利用時まで遅延する。初回の利用が「DLLのロード完了後に呼ばれる通常のAPI」から起きる限り、初期化はローダーロックの外で実行され、Windows APIのほぼ全体を安全に使えます。初回アクセスの排他には
INIT_ONCE(ワンタイム初期化)やC++のマジックスタティック(関数内static)が使えます。ただし遅延は万能薬ではありません ── その初回アクセスをDllMainや静的初期化子の中から行えば、初期化子は結局ローダーロック下で走り、同じ制約に戻ります。 - 早期に検出すべき失敗だけを例外にする。設定ファイルが壊れているならロード自体を失敗させたい、という要件はありえます。その場合も「試みてすぐ失敗する」最小限にとどめます。
- DLL_PROCESS_ATTACH で
DisableThreadLibraryCallsを検討する。スレッド通知を使わないDLLなら、通知コスト自体を消せます(静的CRT・静的TLS使用時を除く)。5 - Application Verifierで検査する。
DllMain内の危険な呼び出しの多くは、Application Verifierが実行時に検出してくれます。1
flowchart TB
accTitle: DLL初期化の設計指針
accDescr: 初期化はまずコンパイル時の静的初期化にできないか検討し、できなければ初回利用時への遅延を基本とし、ロード失敗として早期検出すべきものだけを最小限DllMainに残す
q1{"コンパイル時に決められる?"} -->|"はい"| s["静的初期化にする"]
q1 -->|"いいえ"| q2{"失敗をロード時に検出すべき?"}
q2 -->|"いいえ"| lazy["初回利用時に遅延する(基本)"]
q2 -->|"はい"| min["最小限だけDllMainで行う"]
lazy -.-> once["INIT_ONCEや関数内staticで排他"]
図6: 判断の順序は「静的にできないか→遅延できないか」で、DllMainに残すのは早期検出が必要な最小限だけ。
DisableThreadLibraryCalls の適用判断は、次の分岐で機械的に決められます。
flowchart TB
accTitle: DisableThreadLibraryCallsを呼ぶかの判断
accDescr: 静的CRTとリンクしているDLLでは呼んではならず、静的TLSが有効な場合は呼び出し自体が失敗するため呼ばず、どちらでもなくスレッド通知を使わないDLLならDLL_PROCESS_ATTACHで戻り値を確認しつつ呼んで通知コストを削減できる
q1{"静的CRTとリンク?"} -->|"はい"| no2["呼んではならない"]
q1 -->|"いいえ"| q2{"静的TLSを使用?"}
q2 -->|"はい"| eff["呼んでも失敗する(FALSE)"]
q2 -->|"いいえ"| q3{"スレッド通知が必要?"}
q3 -->|"いいえ"| yes["ATTACHで呼ぶ(戻り値を確認)"]
q3 -->|"はい"| keep["呼ばずに通知を処理"]
図7: 静的CRT・静的TLS・通知の要否の3条件で、呼ぶべきかが一意に決まる。
アンロード時のスレッド停止には、公式が具体的なプロトコルを示しています。DLL_PROCESS_DETACH(FreeLibraryによるアンロード時)でワーカースレッドの終了を「待つ」のではなく、(1) イベントで終了を合図し、(2) スレッド側は仕事を整合状態まで畳んで合図を返し無限待機に入り、(3) DllMain 側は整合状態を確認してから TerminateThread で畳む ── という形です。3 乱暴に見えますが、「DllMain の中でスレッドの自然終了を待ってはいけない」という制約の中での現実解として文書化されています。
sequenceDiagram
accTitle: アンロード時のスレッド停止プロトコル
accDescr: DllMainはイベントでワーカースレッドに終了を合図し、ワーカーは仕事を整合状態まで畳んで合図を返して無限待機に入り、DllMainは整合状態を確認してからスレッドを終了させる
participant D as DllMain(DETACH処理)
participant W as ワーカースレッド
D->>W: イベントで終了を合図
W->>W: 仕事を整合状態まで畳む
W->>D: 整合完了を合図して無限待機
D->>W: TerminateThreadで終了させる
Note over D,W: 自然終了を待たないのでデッドロックしない
図8: 「自然終了を待つ」代わりに「整合状態の合図を待って打ち切る」ことで、ローダーロックとの衝突を避ける。
そもそも論として、アンロードされうるDLLでスレッドを持つ設計自体を避け、スレッドの所有をEXE側に寄せるのが最も安全です。
プロセス終了時のDLL_PROCESS_DETACHは逆に、何もせず返すのが理想です。この時点で他のスレッドはすべて強制終了済みで、依存するDLLやランタイムの状態も当てになりません。ここで凝った処理をすればデッドロックやクラッシュの原因になるだけです。永続化が必要なデータは、アプリ本来の終了処理の中で書き出しておくべきで、この通知を当てにしないでください。3
6. 遭遇したときの調べ方
ローダーロック絡みのハングには、分かりやすい指紋があります。
ハング中のダンプでスタックを見る。固まった瞬間のダンプを取り、各スレッドのスタックを確認します。ntdll.dll のローダー関数(Ldr で始まる関数群)の中でロック待ちしているスレッドと、DllMain や静的初期化子(dynamic initializer)の中で別の何かを待っているスレッドの組が見つかれば、ほぼ確定です。LoadLibrary の呼び出し中で止まっているスレッドも典型的な登場人物です。
flowchart TB
accTitle: ローダーロック絡みのハングの指紋
accDescr: ハング中のダンプで、ntdllのローダー関数の中でロック待ちしているスレッドと、DllMainや静的初期化子の中で別の何かを待っているスレッドの組が見つかれば、ローダーロックのデッドロックとほぼ確定できる
dump["ハング中のダンプ"] --> t1["Ldr系関数でロック待ちのスレッド"]
dump --> t2["DllMain・静的初期化子内で待つスレッド"]
t1 --> pair{"両方いる?"}
t2 --> pair
pair -->|"はい"| conf["ローダーロックのデッドロックとほぼ確定"]
pair -->|"いいえ"| other["別系統のハングとして調査"]
図9: ローダーロック絡みのハングには「Ldr待ち+DllMain内待ち」という分かりやすい指紋がある。
「タイミング依存」の性質を疑う。ローダーロックのデッドロックは、DLLのロードとスレッドの開始・終了が重なった瞬間にだけ成立します。「起動時にたまに」「特定マシンでだけ」「サービスとして動かしたときだけ」という再現条件は、この種の問題のサインです。
予防的な検査を回す。Application Verifierを有効にしてテストを流すと、DllMain 内の危険な呼び出しを実行時に検出できます。1 C++/CLIなら警告C4747を無視しない、DllMain から到達する関数のレビューでは「間接的に LoadLibrary を呼ぶ関数」(COM初期化、一部のCRT機能、遅延ロードインポートなど)に注意する、といった観点をレビュー項目に加えておくと、事故を作り込む前に拾えます。遅延ロード(delay-load)されたインポート関数の初回呼び出しが内部で LoadLibrary になることは、見落としやすいポイントです。
7. まとめ
DllMainはローダーロック(プロセスに1本、全DLL通知を直列化するロック)を保持したまま呼ばれる。制約はすべてここから導かれる。- 禁止事項の核は「
LoadLibrary/FreeLibraryを呼ばない」「他スレッドと同期しない」「Kernel32以外のDLLに依存する関数を呼ばない」。CRT経由で動く静的オブジェクトのコンストラクタ・デストラクタも同じ制約を受ける。 - 設計の基本方針は遅延。静的にできる初期化は静的に、残りは初回利用時に。
DisableThreadLibraryCallsとApplication Verifierを活用する。 - アンロード時のスレッド停止は公式のプロトコル(合図→整合確認→終了)に従う。プロセス終了時のDLL_PROCESS_DETACHは空が理想。
- C++/CLIではローダーロック下のMSIL実行が固有の地雷。
DllMainの呼び出し木はネイティブコンパイルを徹底する。
DllMain の制約は、一見すると理不尽な禁止事項の羅列に見えます。しかし「ローダーロックという最上位ロックを持ったまま呼ばれる」という1点を押さえれば、すべての禁止事項は同じ原理の言い換えだと分かります。原理で覚えておけば、ドキュメントに載っていない境界事例に出会ったときも、「これはロックを持ったままやってよい仕事か?」という正しい問いを立てられるはずです。
関連記事
- Windows DLL名前解決の仕組み - 検索順序とSxS
- ネイティブDLLをC++/CLIでラップする実務
- マルチスレッドの実務ベストプラクティス C++編
- スプリアスウェイクアップ ── 条件変数が「通知なしに目覚める」理由とWindowsでの正しい待ち方
- WinDbg + SOSでクラッシュダンプを読む ── 収集した後の実務解析入門
- COM STA/MTA の基礎知識 - スレッドモデルとハングを避ける考え方
関連する相談領域
合同会社小村ソフトでは、起動時・DLLロード時のハングやデッドロックの原因調査(ダンプ解析)、DllMain・静的初期化まわりの設計レビュー、C++/CLIラッパーやプラグインDLLの安全な初期化設計への改修を扱っています。「特定環境でだけ起動が固まる」といった再現困難な段階からでもご相談いただけます。
参考リンク
-
Microsoft Learn, Dynamic-Link Library Best Practices. DllMainがローダーロック保持中に呼ばれるため呼べる関数に重大な制約があること、理想のDllMainは空のスタブであり初期化はできる限り遅延すべきこと、コンパイル時の静的初期化の推奨、早期検出が必要な失敗だけを最小限行うこと、Application VerifierによるDllMain内の典型的な誤りの検出について。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, DllMain entry point. エントリポイントでは単純な初期化・終了処理だけを行うべきこと、LoadLibrary / FreeLibraryを呼んではならない理由(ロード順序の循環と初期化前・終了後のDLL使用)、Kernel32.dllは必ずロード済みなので他のDLLをロードしない範囲で呼べること、安全な関数の網羅的な一覧は存在しないこと、User・Shell・COM関数がアクセス違反を招くこと、DLL通知が直列化されているため他スレッド・他プロセスとの通信がデッドロックを招くこと、CRTとリンクした場合は静的オブジェクトのコンストラクタ・デストラクタにも同じ制約が適用されることについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. DllMain内でスレッドの終了を待つとデッドロックする構造(スレッド終了のDLL_THREAD_DETACH通知がローダーロックを必要とする)、アンロード時のスレッド停止プロトコル(イベントで合図し整合状態を確認してから終了する)、プロセス終了時のDLL_PROCESS_DETACHでは他スレッドが強制終了済みでアドレス空間の一貫性が保証されず理想のハンドラーは空であること、DllMainでスレッドを作ると初期化未完のまま通知が滞留し問題を生むことについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Initialization of Mixed Assemblies. ローダーロック下でMSILを実行してはならないこと、DllMainとその呼び出し木をMSILにコンパイルしてはならず#pragma unmanagedで対処すること、DllMainが直接MSILを実行しようとすると警告C4747が出るが別モジュール経由の間接実行は検出できないこと、静的オブジェクトの動的初期化子も同じ問題を起こしうることについて。 ↩ ↩2
-
Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h). DLL_THREAD_ATTACH / DLL_THREAD_DETACH通知を無効化してスレッド生成・破棄時のオーバーヘッドを減らせること、静的CRTとリンクしたDLLから呼んではならないこと、静的TLS(thread_localや__declspec(thread))が有効な場合は最適化が行われないことについて。 ↩ ↩2
-
Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. ロック階層を定義して常に同じ順序で取得すべきこと、ローダーはローダーロックを取得してからDllMainを呼ぶためローダーロックはロック階層の最上位に置くべきこと、GetModuleFileNameのように間接的にローダーロックを取るAPIと私有ロックの取得順序を守ること、ロック順序逆転によるデッドロックの実例について。 ↩ ↩2
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
Win32スレッドプールAPI ── CreateThreadpoolWorkで「スレッドを作らない」並行処理
ネイティブコードでCreateThreadを乱立させていませんか。Vistaで刷新されたWin32スレッドプールAPIのwork・timer・wait・ioの4オブジェクト、クリーンアップグループ、コールバックでの禁止事項までを一次情報にもとづいて解説します。
「応答なし」の正体 ── Windowsがアプリを「固まった」と判定する仕組みと、固まらない設計
Windowsの「応答なし」は、ウィンドウが5秒間メッセージを取り出さないとOSが判定し、幽霊ウィンドウに差し替える仕組みです。判定の内部動作から、固まる定番原因、UIスレッドから重い処理を追い出す設計、ハングの調査手順まで解説します。
スプリアスウェイクアップ ── 条件変数が「通知なしに目覚める」理由とWindowsでの正しい待ち方
条件変数のwaitは通知が来ていなくても目覚めることがあります(スプリアスウェイクアップ)。仕様がそれを許す理由をWindowsの実装から解き明かし、whileと述語で書く正しい待ち方をWin32・C++・C#のコードで示します。
名前付きパイプの実務 ── Windowsプロセス間通信の定番を設計からセキュリティまで
Windowsのプロセス間通信の定番・名前付きパイプを実務目線で解説します。バイト/メッセージモードの選択、複数クライアントを捌くサーバー設計、ACLと偽装のセキュリティ、.NETのNamedPipeStreamまで、一次情報にもとづき整理します。
スリープ復帰で壊れるアプリ ── 電源イベントの仕組みと復帰に強い業務アプリの作り方
ノートPCを開いたら業務アプリの通信が切れていた――原因はスリープを想定しない設計です。WM_POWERBROADCASTによる通知の流れ、Modern Standbyの挙動、切断・再接続の設計、スリープ抑止と調査コマンドまでを一次情報で解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
不具合調査 / 長期稼働テーマ
再現しにくい不具合、通信停止、長期稼働障害、失敗パス検証を整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
不具合調査・原因解析
再現しにくい障害、長期稼働後の不具合、通信停止などの調査を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- DllMainでは本当に何もできないのですか?
- 「何もするな」は誇張ではなく公式の設計方針で、理想のDllMainは空に近いスタブだとMicrosoft自身が述べています。安全なのは、DllMainの時点で必ずロード済みであるKernel32.dllの、他のDLLをロードしない範囲の関数だけです。たとえばクリティカルセクションやミューテックスの作成、TLSの利用はできます。逆にLoadLibrary/FreeLibrary、他スレッドとの同期、User32・Shell・COMなどの関数呼び出しは、デッドロックやアクセス違反の原因になるため禁止です。判断に迷う初期化は「DllMainでやらず、最初に使われるときまで遅延する」のが正解です。
- C++のグローバル変数(静的オブジェクト)のコンストラクタもDllMainの制約を受けますか?
- 受けます。DLLがCRT(C++ランタイム)とリンクされている場合、グローバル・静的オブジェクトのコンストラクタとデストラクタはCRTが提供するエントリポイント経由で、実質的にDllMainの一部として実行されます。つまりコンストラクタの中でLoadLibraryを呼ぶ、他スレッドを起動して完了を待つ、COMを初期化するなどはすべてDllMainでやるのと同じ危険があります。複雑な初期化を持つグローバルオブジェクトは、ポインターにしておいて初回アクセス時に生成する、関数内staticにするなど、実行タイミングをDllMainの外へずらしてください。
- DisableThreadLibraryCallsは呼んだほうがよいですか?
- 条件付きで有効です。DLL_THREAD_ATTACH/DETACH通知が不要なDLLなら、DLL_PROCESS_ATTACHでDisableThreadLibraryCallsを呼ぶことでスレッド生成・破棄のたびの通知を止められ、スレッドを頻繁に作るプロセスでのオーバーヘッドを減らせます。ただし2つ例外があります。静的CRTとリンクしているDLLでは呼んではいけません(静的CRTがスレッド通知を必要とします)。また、thread_localや__declspec(thread)による静的TLSが有効な場合は呼び出し自体が失敗しFALSEが返るため、戻り値の確認を習慣にしてください。動的リンクのCRTを使う一般的なDLLで、スレッド通知に依存する処理がないことを確認して使ってください。
- C++/CLI(マネージド混在)のDLLで起動時にハングするのはなぜですか?
- ローダーロック保持中にMSIL(マネージドコード)を実行しようとしていることが典型原因です。C++/CLIの混在アセンブリでは、DllMainやそこから呼ばれる関数、グローバル変数の動的初期化子がMSILにコンパイルされていると、ローダーロック下でCLRの初期化や別アセンブリのロードが必要になり、デッドロックしえます。コンパイラはDllMainが直接MSILを実行しようとすると警告C4747を出しますが、別モジュール経由の間接的な実行は検出できません。対策はDllMainとその呼び出し木を#pragma unmanagedでネイティブコンパイルすること、そもそもDllMainを持たないことです。
- DLL_PROCESS_DETACHでリソースの後始末をしてもよいですか?
- 「プロセス終了時」と「FreeLibraryによるアンロード時」で答えが変わります。プロセス終了時のDLL_PROCESS_DETACHでは、他のスレッドはすでに強制終了されており、アドレス空間が一貫している保証もないため、メモリ解放のような後始末はむしろ危険で、公式も「理想のハンドラーは空」としています。永続化が必要なデータはアプリ本来の終了処理で書き出しておき、ここでは基本的に何もせず返すのが安全です。一方、FreeLibraryによるアンロード時はプロセスは生き続けるので、スレッドの停止・ハンドルのクローズなどの完全な後始末が必要です。ただしDllMain内でスレッドの終了を待つとデッドロックするため、公式が示す「合図して整合状態まで待ち、最後はDllMainの外で処理する」プロトコルに従う必要があります。