DLLの闇 (2)

id:NyaRuRu:20060715 の続き.
分かったこと.

Dependency Walker は素晴らしい.

さあみなさん,目の前のバービー人形にでも DLL Hell を説明してみてください.
「system32 ディレクトリの mfc42.dll などが,古いバージョンや新しいバージョンのもので置き換えられてしまうことで,今まで動いていたアプリケーションが実行時エラーを起こすようになる現象」というのではどうも生ぬるい気がします.



Windows の DLL の仕組み,特に PE ヘッダに記述するいわゆる「暗黙のリンク」は,ベースファイル名しか記述しないため,名前空間の競合やロード順序によっては簡単に破綻してしまうことがあります.例えば "Security.dll" という名前の DLL が引き起こした混乱について,The Old New Thing で紹介されています.

Don't name your DLL "Security.dll"
http://blogs.msdn.com/oldnewthing/archive/2004/07/02/171769.aspx

Dependency Walker は,この名前解決をシミュレートしてくれます.単に PE ファイルのヘッダをダンプして,どの DLL に暗黙にリンクしているか調べるだけなら dumpbin で十分です.Dependency Walker は,それだけではなく,その環境で exe ファイルを実行したらどの DLL がロードされるかについて,ある程度の情報を与えてくれるところが優れています.
前回作った SxS による ws2_32.dll の置き換えサンプル (id:NyaRuRu:20060711#p1) を,次のように配置し,Dependency Walker 2.1.3623 に test.exe を読み込ませてみます.環境は Windows Vista build 5456 日本語版です.

  • d:\test\test.exe
  • d:\test\ws2_32.dll

Dependency Walker 起動直後の状態では,モジュールのパスが表示されていません.

起動直後の状態

そこで,F9 を押してパスを表示させます.

パスを表示させた状態

exe ファイルと同じディレクトリにある ws2_32.dll がロードされることが分かります.
参考までに Windows 2000 SP4 での実行画面.(ただし,c:\test ではなく d:\test に配置しています)

パスを表示させた状態 (Windows 2000 SP4)

Windows 2000 は manifest を使用した SxS コンポーネントに対応していません.よく見れば msvcr80.dll のパスが Vista 環境と異なっています.プライベートな ws2_32.dll が呼び出されるのは純粋に DLL の検索順序によるものと予想されます.
これを確かめるために Dependency Walker のメニューから,Options → Configure Module Search Orders を選んでみましょう.まずは Vista 環境のものから.

Configure Module Search Orders (Windows Vista build 5456)

test.exe の manifest がきちんと反映されていて,SxS コンポーネントが検索順序の上位に来ていることが分かります.また,ws2_32.dll が KnownDLLs として扱われていること*1にも注意が必要です.manifest ファイルで ws2_32.dll を指定しなかった場合,アプリケーションディレクトリよりも KnownDLL の方が検索順序で上位にくるため,システムディレクトリの ws2_32.dll がロードされることになります.
次に Windows 2000 SP4 の場合.

Configure Module Search Orders (Windows 2000 SP4)

Windows 2000 は manifest を使用した SxS コンポーネントに対応していないのでこれらが検索順序から消えています.一方で,ws2_32.dll は KnownDLLs に含まれません.結果として,アプリケーションディレクトリの ws2_32.dll が読み込まれることになります*2



Dependency Walker を活用したデバッグ方法が,Mozilla の開発者向け資料に紹介されています.
Dependency Walker (depends.exe) の使用方法
この資料を読むまで知りませんでしたが,Dependency Walker にはプロファイル機能もあって,プロセス実行中の LoadLibrary や GetProcAddres の呼出しをフックし,そのログから依存グラフを再構成することができるようです.また,一連の作業は CUI からバッチ処理することもできるとのことでした.
というわけで,Mozilla の開発者向けページにあるように,DLL のロードで困ったことがあれば,Dependency Walker のプロファイルログを作成し,しかるべきところに投げてみるという方法は,かなり有効かもしれません.

*1:ただし,レジストリの KnownDLLs の項目に ws2_32.dll は含まれていない.この挙動は,http://support.microsoft.com/kb/164501/en-us に記載されている「KnownDLL から暗黙にリンクされる DLL」についての記述を反映したものと考えられる.ただし,KnownDLLs の挙動は OS によって変化することにも注意.詳しくは『[http://www.microsoft.com/technet/technetmag/issues/2007/09/WindowsConfidential/default.aspx?loc=jp/:title=]』参照のこと.

*2:Windows 2000 当時は,アプリケーションディレクトリ→カレントディレクトリ→システムディレクトリの順位だったことに注意.see SafeDllSearchMode.id:NyaRuRu:20040931#p2

DLLの闇 (3)

実際にプロセスを起動してプロファイリングを行う機能というのは,静的解析の考慮漏れに対するある意味での切り札です.そんな切り札も用意してくれている以上,あまり重箱の隅をつついても仕方がありませんが,一応 Dependency Walker の静的解析についても少し補足しておきましょう.
Dependency Walker がデフォルトで考慮しない要素がいくつかあります*1.これらの要素については,Dependency Walker の Module Search Orders を手で編集することで,かなりの割合を反映させることが出来ます.

  • DLL 検索でのカレントディレクトリの存在と,その優先順位
  • .local という拡張子を持ったファイルまたはディレクトリによる DLL/COM リダイレクション

また,暗黙のリンクではなく,LoadLibrary による実行時の明示的なロードも,静的解析が不可能に近い要素です.
『Advanced Windows 第4版(asin:4756138055)』第22章「DLLの注入とAPIフック」にあるような,複雑な方法による DLL の注入が行われていると,(静的な解析では)予期しない問題が発生する可能性があります.また DllMain 中から LoadLibrary を呼び出すといった,推奨されない行為を行ってしまっている例もしばしば見られます*2

以下の操作は、大部分の状況で、DllMain 関数内部で実行すると安全ではないと明確に認識されています。

  • 直接的または間接的な、LoadLibrary 関数、LoadLibraryEx 関数、または FreeLibrary 関数の呼び出し。
  • レジストリ関数の呼び出し。
  • Kernel32.dll 内に存在しないインポートされた関数の呼び出し。
  • 他のスレッドまたはプロセスとの通信。

この他,近年の Windows システム DLL で多用される DLL の遅延ロードも事態を複雑化させている要因の 1 つでしょう.
こうなってくると,正しい DLL が呼ばれているかどうかについてのテストフレームワークが欲しくなってきます.設定や状況のパターンが非常に多く毎回資料を調べるだけでは追いつかないのと,そもそも MSDN の書き方に曖昧な点が存在するという理由から,C# でも専用言語でも何でもいいですが,とにかくテストを自動化しないことには対処できない規模だと感じます.



Joseph M. Newcomer 氏は,複雑怪奇な LoadLibrary の挙動をテストするために,LoadLibrary Explorer というツールを作成されています.
http://www.flounder.com/loadlibrary_explorer.htm
このツールを使えば,複数のディレクトリにターゲット DLL を配置し,LoadLibrary API でどの DLL が読み込まれるかを,様々な設定で実験することが可能です.
氏はこのツールを使い,実際にドキュメントの不具合をいくつか発見したと書かれています.

Bug summary

Using the LoadLibrary Explorer, I have determined the following bugs appear in the Microsoft documentation:

  • Dynamic-Link Library Redirection, the use of the .local file or directory, does not work in Windows XP SP2 or Windows Server 2003.
  • SafeDllSearchMode does not work even for load-time dynamic linking
  • LOAD_WITH_ALTERED_SEARCH_PATH does not work; if the DLL is not found in the directory with the given path, it does not change the behavior of either implicit or explicit dynamic linking.

ただ,手元の環境で試した限り,これらの挙動はドキュメントの記述の曖昧さに原因があるようです.

  • MSDN にはどの時点で .local ファイルまたはディレクトリが存在している必要があるのか記述がない.(LoadLibrary Explorer は,プロセス起動後にこれらのファイルを作成する)
  • SafeDllSearchMode の設定変更後,システムを再起動する必要がある.(LoadLibrary Explorer は,レジストリ値を変更した直後に動作を検証している)
  • LoadLibraryEx の解説にある,以下の 2 つのルールの優先順位が不透明.
    • 絶対パスで渡されたファイルが存在しなかった場合 → 呼出しは失敗する
    • LOAD_WITH_ALTERED_SEARCH_PATH が指定されたとき,最初の検索ディレクトリにファイルが存在しなかった場合 → 次の検索ディレクトリを探す



他にも MSDN の記述には気になるところがあって,先ほど実験しているときも,こんな記述で随分と混乱してしまいました.

Known DLLs cannot be redirected. For a list of known DLLs, see the following registry key:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs. The system uses Windows File Protection to ensure that system DLLs such as these are not updated or deleted except by operating system (OS) updates such as service packs.

Windows 2000:
Known DLLs can be redirected in this OS.

If the application has a manifest, then any .local files are ignored.

Windows 2000:
Manifests do not affect DLL redirection.

私は最初,「Known DLLs に含まれる DLL は,.local によるリダイレクトから除外される」と思っていたのですが,Windows XP で試しても Windows Vista で試しても,リダイレクトは行われています.さらなる仕様変更が行われているのでなければ,この記述は,「リダイレクトは働くけど,リダイレクト可能なようには設計されていないよ」と読まざるを得ません.ドキュメントよりもアルゴリズムをあらわす擬似コードを書いてくれた方が楽だったかも知れません.

*1:.NET のアセンブリルックアップについても Dependency Walker では解析が難しいかと思います.ただし .NET の世界のみであれば,資料がしっかりしていますし,それ程苦労することはないでしょう.また,混合モードアセンブリの解析では Dependency Walker が役立つと予想されます.

*2:『Advanced Windows 第4版(asin:4756138055)』22.2 「レジストリを使ったDLLの注入」で紹介されているように,user32.dll がこのルールを破ってしまっているという例もあります.user32.dll は,DLL_PROCESS_ATTACH 通知を受け取ると,レジストリの [google:AppInit_DLLs] に指定された DLL をLoadLibrary で呼び出します.

DLL の闇 (4)

折角なので,先ほどの The Old New Thing に記事について少し触れておきましょう.人ごとではないことが分かるかと思います.

Don't name your DLL "Security.dll"
http://blogs.msdn.com/oldnewthing/archive/2004/07/02/171769.aspx

事件は,ASP.NET 開発で,Security というプロジェクトを作ってしまったところから始まります.デフォルトのままであれば,生成される .NET アセンブリの名前は Security.dll になるでしょう.不幸だったのは,システムディレクトリにもまったく同名の DLL が存在したこと,そしてこの DLL が実際に裏で使われていたということでした.その結果,database との接続がうまくいかなくなった,というわけです.
.NET では,.NET 内で閉じる限り,アンマネージドコードな DLL に比べ比較的一貫したモジュール探索や同一性検査が行われます.しかし,拡張子 DLL を使用している以上,今回のように Win32 DLL としての側面が,アンマネージドコードな世界で問題を引き起こすことがあります.



例として,Managed DirectX でありそうなシナリオを示します.
まず,Sample Browser から,Tutorial 1: Create a Device をコピーしてきます.これはまあ問題なく動くとしましょう.
次に,Direct3D 絡みの処理をまとめるために,D3D9 というクラス ライブラリ プロジェクトを作成します.

D3D9 というクラス ライブラリ プロジェクトを作成

さらに,元々のプロジェクトの参照設定で,この D3D9 プロジェクトを追加します.

D3D9 プロジェクトを参照設定に追加

この段階で,Crtl+F9 または「デバッグ」→「デバッグなしで開始」を選択し,プロセスを起動してみるとどうなるでしょうか?

起動

とまあこうなるわけです.



結論としては,.NET 開発においても,アセンブリ名には十分注意する必要があります.システムディレクトリに存在する DLL 名は,極力避けた方が賢明でしょう.同名の DLL を使用するアンマネージドコードがあった場合,問題になり得ます.あるいは,競合しそうな短い名前の DLL は,exe ファイルと同じディレクトリに配置しないようにするべきです.
参考までに,Vista build 5456 の system32 以下の DLL 一覧を置いておきます.
http://www.dwahan.net/nyaruru/hatena/vista-5456-system32-dll.txt
例えば P2P という名前のプロジェクトを作っていて,生成アセンブリ名が P2P.dll というのに心当たりがある方,気をつけた方がいいかもしれませんよ.