DLL Hell の終焉

Rick Anderson
Microsoft Corporation

January 2000

要約: 3 種類の 「DLL Hell」 を説明します。また、DLL の互換性の問題を解決するため、DLL Universal Problem Solver (DUPS) パッケージをどのように使用できるかを検討します。

日本語版編集注: この記事の中で紹介されている「DUPS パッケージ」は現状のままの状態でサンプルとして提供されています。従って、サポート対象外であり、該当のツールを利用した結果生じた問題については、何ら保証いたしませんのでご注意ください。

目次

はじめに
なぜ DLL なのか?
DLL Hell への Windows 2000 のアプローチ
DLL Hell への攻撃
DLLhell データベースのデータ参照
DUPS がすでに成し遂げたこと
将来の方向性
DLL Hell は本当に終わったのか?

はじめに

社交場で医者を紹介されると、よく起きる病状をその医者に伝え、すぐに治してもらえると思う人が多いものです。実際、その医者が形成外科医で、自分は背中が痛いのだとしても関係ありません。彼らにとっては、医者は医者です。私も同じような状況に遭遇することがよくあります。初めての人々に紹介された場合、私にコンピュータの問題を話せば、すぐに解決してくれると思ってしまう人がいるのです。最も多く報告される問題は、「DLL Hell」 として知られています。これは、新しいアプリケーションをインストールすると、1 つまたは複数の既存プログラムが動かなくなってしまうというものです。

ある日、隣の席にいるハンクが垣根越しに身を乗り出して、こう言いました。「FAT32 ファイル システムにしたくて、ドライブ変換ツールを実行しようとしてるんだが、ツールをスタートするたびにエラー メッセージが出てしまうんだ。」

"The ordinal 968 could not be located in the dynamic-link library MFC42.dll."

このシステムをざっと調べたところ、5 つのディレクトリに MFC42.dll がインストールされていて、全部が同じ旧バージョンのものでした。「これがいけないんだよ、ハンク。」と私は伝えました。「何かいんちきなインストーラが、システム バージョンの MFC42.dll を、必要な機能を持っていない古いバージョンで置き換えてしまったんだ。」ハンクは興奮して、こう言い返してきました。「きみのコンピュータからぼくのコンピュータに、 MFC42.dll を単純にコピーできないのかい?」私は「う~ん、できる場合もある。」と答えました。「でも、DLL は、まとまって動かなければならないセットになっているんだ。この DLL を更新するサービス パックをインストールして、整合したセットにしないといけないね。」私は、この DLL を更新している MicrosoftR Visual StudioR 6.0 Service Pack 3 をインストールすることに決めました。しかし、Visual Studio 6.0 SP3 には Visual Studio コンポーネントが必要です。そのため、まずハンクのマシンに Visual Basic 6.0 をインストールしてから、サービス パックをインストールしなければなりませんでした。SP3 のアップグレードが完了した後、Visual Basic 6.0 をアンインストールしました。 Visual Basic 6.0 のアンインストールでは、(MicrosoftR Windows NTR サービス パック インストーラ以外のすべてのインストーラと同様に、) ハンクが必要とする共有システム DLL の整合セットは削除されません。つまり、Visual Basic 6.0 に固有のリソースのみが削除されたのです。

ハンクの頭痛のタネは、よく見かける DLL にとりつく悪魔のせいでした。つまり、システム ディレクトリに DLL をコピーする前に、バージョンをチェックしない行儀の悪いインストール プログラムを指します。もしそのインストール プログラムが、MFC42.dll の既存のバージョンとインストールするバージョンをきちんと比較していれば、古い MFC42 のロードは行われず、したがって問題を起こすこともなかったのです。これを タイプ I 、または 旧バージョンによる DLL 置換 の DLL Hell と呼ぶことにします。この問題は、Windows 9x ユーザー、特にフリーソフトウェアをダウンロードしたり、または友人からプログラムをコピーしているユーザーに特に多く見受けられます。最近の商用ソフトウェアでは、DLL を置き換える前に必ずバージョンをチェックするようになにっているので、このような問題は殆ど起こりません。

Windows NT では、新しい DLL に意図しない、あるいは予期しない機能変更が加えられている場合に、しばしば DLL Hell が発生します。これを、 タイプ II 、または、 副作用の DLL Hell と呼ぶことにします。DLL は下位互換性があるべきことになっていますが、100 パーセントの下位互換性を保証することは現実的には不可能です。

副作用が引き金となる問題のうち、一般によく知られている例は、Windows NT 4.0 の Service Pack 4 で発生したものです。多くのカスタマから、SP4 をインストールした後、アプリケーションでメモリのアクセス違反が発生するという報告がありました。私が調べたほぼ全てのケースで、クラッシュしたアプリケーションは、再割り当てのため、呼び出しによって開放または移動されたメモリ アドレスを使っていました。旧バージョンのヒープ マネージャでは、プログラマはアクセス違反を起こさずに、たいていこのコーディング上のエラーを逃れることができました。SP4 では、そのバグがすぐに表面化してしまうのです。副作用 DLL Hell は、新しい DLL のバグではなく、DLL の副作用(よくある例では DLL の持つ既存のバグ)に依存しているアプリケーションによって起こります。そして、その副作用はサービスパックなどによって解消してしまう可能性がある訳です。 アプリケーションが依存する副作用は、SP4 のメモリ バグのようなコーディング エラーではない場合もあります。例えば、クラスのデータ メンバをドキュメント化せずに使っているプログラマがたくさんいます。今後のバージョンでは、そのメンバをプライベートにするか、あるいは別の構造体に移動するかもしれません。

3 つめの DLL Hell 問題は、新たなバグを含む新しい DLL バージョンをインストールすることに起因します。 タイプ III による DLL Hell は一番頻度が低いのですが、起きないとはいいきれません。

なぜ DLL なのか?

なぜかを問う前に、ここで、DLL とは何なのかをきちんと定義すべきでしてみましょう。DLL は、 dynamic-link library (ダイナミック リンク ライブラリ) の頭文字をとったものです。DLL は、実行時にアプリケーションがリンクするソフトウェア コンポーネントです。標準の strlen 関数 (strlen は渡された文字列の長さを返します) を使うプログラムを書き、Msvcrt.dll へ動的にリンクさせる場合、そのプログラムの EXE には strlen に対する命令は含まれず、Msvcrt.dll 中の strlen のアドレスへの呼び出し命令が含まれます。プログラムを実行すると、Msvcrt.dll 中のメソッドが最初に呼び出されたときに、Msvcrt.dll がロードされます。

DLL について、さらに情報を入手したい場合は、MSDN ライブラリを検索してください。

Unix は、伝統的に完全にリンクされたイメージでアプリケーションを出荷しています。現在の Unix は共有ライブラリをサポートしていますが、多くの Unix ベンダが、引き続き静的にリンクされたイメージを出荷しています。この場合、アプリケーションは完全に自立しているため、新しいライブラリやアプリケーションをインストールすることで、既存のプログラムが動かなくなるようなことはありません。アプリケーション ベンダは、別のソフトウェアのインストールによって自分たちの製品が壊されるという心配をせずに済むのです。そこで、この質問になります。「DLL のアプローチが静的にリンクされたイメージのアプローチよりも堅牢でないのなら、なぜ DLL を使うアプリケーションを出荷したいと思うのでしょうか?」

主張 1: DLL は、ディスク スペースを節約します。ほとんどすべてのアプリケーションは、メモリや他のリソースを割り当てる必要があります。もしコンピュータ上に Msvcrt.dll を使うアプリケーションやユーティリティが 501 個あって、これらが静的にリンクすると、少なめに見積もっても Msvcrt.dll のサイズの 500 倍のディスク スペースを無駄にすることになります。

注意: 使用しているファイル システムによっては、実際の浪費スペースがさらに多くなる場合があります。FAT16 ファイル システムでは、整数単位の固定長ファイル ブロックでファイルを格納します。ファイルの最後のブロックは完全に埋まらないことがあるため、平均でファイルごとに 2 分の 1 ブロックずつディスク スペースが無駄になります。

現実: DLL は確かにディスク スペースを節約します。しかし、ディスク スペースはただ同然であり、また安くなる一方でしょう。それでもなお、共通のコードが安全に共有可能である限りにおいて、DLL は有益だといえます。

主張 2: DLL はメモリ マッピングと呼ばれる共有テクニックを使って、メモリを節約します。Windows はグローバル ヒープに DLL をロードしてから、その DLL をロードする各アプリケーションのアドレス スペースに DLL のアドレス範囲をマップ しようとします 。10 の異なるプロセスがあって、それらがすべて Msvcrt.dll を使う場合、そのコピーを 10 個ロードする代わりに Msvcrt.dll の同じインスタンスを共有することができます。

現実: ほとんどのプロセスでは、それぞれ特定の DLL をロードし、またその DLL の 1 つのグローバル インスタンスを共有できます。共通の DLL を共有することは、メモリ ロードをかなり節約します。しかし、Windows では、複数プロセスによってロードされた 1 つの DLL インスタンスを常に共有できるわけではありません。

Microsoft Visual C++R あるいはその他のデバッガを使ったことがあれば、おそらく次のようなメッセージを見たことがあるでしょう。

LDR: Automatic DLL Relocation in my.exe 
LDR: Dll abc.dll base 10000000 relocated due to collision with C:\xyz\defg.dll 

DLL が作成されると、ベース アドレスがリンカによって指定され、Windows がプロセスの 32 ビット アドレス スペースのどこにその DLL をロードすべきかを示します。Visual C++ で作成された DLL の省略時の設定は、0x10000000 です。要求されたベース アドレスに対して 0x20000000 を指定する共有 DLL (abc.dll と名付けます) があるとします。アプリケーション a.exe は、現在 0x20000000 にロードされた abc.dll を使って動いています。アプリケーション b.exe も同じ abc.dll を使いますが、0x20000000 にはすでに def.dll をロードしています。そこで OS は abc.dll のベース アドレスを、プロセス b でユニークなアドレスに変更しなければなりません (Rebasing と言います)。プロセス b は abc.dll の Rebasing をしたため、OS は a.exe プロセスと abc.dll を共有できません。したがって、この場合は、abc.dll と静的にリンクするのと同じことになり、メモリは節約されません。

たいていの DLL では、Rebasing をする必要はありません。ですから、DLL は静的リンクで複製される分のメモリを節約します。しかし、現在、メモリ価格は下がり続けているため、メモリの節約は重要な点ではなくなっていくかもしれませんが、RAM がディスクより約 10,000 倍速いことを考えれば、多くのアプリケーションでそれなりのパフォーマンスを得るために DLL は絶対必要です。

主張 3: 通常、バグは単一の DLL に限定されたものであるため、バグ フィックスは簡単に提供できます。イメージ全体を出荷する必要はなく、修正された DLL だけでよいのです。

現実: 静的にリンクされたイメージは 、必ずしも唯一のイメージである必要はありません。アプリケーションが置かれているのと同じディレクトリに、別々のファイルとしてリンクされたライブラリを入れておくことができます。実は、これが Windows 2000 でとられている DLL Hell への 1 つのアプローチです。

堅牢さと効率のどちらをとるかは、アプリケーション、ユーザー、およびシステム リソースによって決まります。アプリケーション ベンダ、ユーザー、およびシステム管理者が、信頼性と経済性のどちらが重要かを決定できるような状況が理想的です。コンピュータ リソースのコストは急速に下がっているため、今、経済性で選んだとしても、来年には間違った決断となるかもしれません。

DLL Hell への Windows 2000 のアプローチ

Windows File Protection

Windows File Protection (WFP) は、権限のないエージェントによってシステム DLL の更新や削除が行われないように保護します。アプリケーションは、システム DLL を置き換えすることができず、サービス パックなどの OS のアップデート パッケージのみがこれを行えます。サービス パックでしか変更できないシステム DLL は、保護された DLL (protected DLL) と呼ばれます。Windows 2000 では、約 2,800 の保護された DLL があります。

同名でバージョンの違う DLL で、保護された DLL を上書きコピーしてシステム ライブラリ (winnt\system32) に格納しようとした場合、コピーは成功したように見え、エラー メッセージも出ません。Windows 2000 では、自動的にオリジナルの DLL のコピーで新しい DLL を置き換えているのです。

DLL がシステム ディレクトリに格納されると、必ず Windows 2000 はディレクトリ変更の通知を受け取ります。その後、変更された DLL が保護された DLL であるかどうかをチェックします。もし DLL が保護されていれば、新しい DLL が有効なデジタル認証を持っているかチェックします。新しい DLL が有効なデジタル認証を持っていなければ、Windows 2000 は winnt\system32\dllcache から winnt\system32 に、オリジナルの DLL をコピーします。WFP は、インストーラがシステム DLL を変更できないようにしています。もちろん、Office や Visual Studio などの Microsoft 製品であっても、システム ディレクトリ中の保護された DLL をアップグレードすることはできません。

WFP では保護された DLL に関して、タイプ I の問題を完全に排除し、また、アプリケーションのアップグレードやインストールによって生じる、タイプ II および III の問題を防ぎます。

プライベート DLL

プライベート DLL は、特定のアプリケーションによってインストールされ、そのアプリケーションだけに使用される DLL です。たとえば、自分が SuperApp.exe プログラムの責任者であると思ってください。SuperApp.exe を Msvcrt.dll バージョン x.x と Sa.dll バージョン y.yでテストしました (Sa.dll は、Microsoft の DLL ではなく、複数の異なるアプリケーションによって配布されている、サード パーティの DLL です)。SuperApp.exe が、いつも確実に Msvcrt.dll バージョン x.x と Sa.dll バージョン y.y を使うようにしたいと考えています。そのために、インストーラで SuperApp.exe、Msvcrt.dll バージョン x.xSa.dll バージョン y.y を、.\SuperApp ディレクトリに格納します。それから、Windows 2000 に SuperApp.exe はこれらのプライベート DLL を使用しなければならないことを知らせます。SuperApp.exe が Windows 2000 システムで実行されると、システム ディレクトリとパス ディレクトリより前に、.\SuperApp ディレクトリ内で DLL を探します。将来、Msvcrt.dll をアップグレードするサービス パックが出されても、SuperApp.exe が動かなくなることはありません。なぜなら、共有バージョンの Msvcrt.dll を使用していないからです。異なるバージョンの Sa.dll をインストールする他のアプリケーションが、SuperApp に影響を与えることもありません。なぜなら、SuperApp はプライベート バージョンの Sa.dll を持っているからです。

また、プライベート DLL は、 (Side-by-Side DLL) とも呼ばれています。これは、プライベート コピーの DLL が特定のアプリケーションで使用されるときに、システム DLL が他のアプリケーションで使われているからです。WordPad と SuperApp を同時に実行すると、WordPad と SuperApp が同じバージョンの Msvcrt.dll を使っている場合でも、2 つの Msvcrt.dll コピーがメモリにロードされます (それで、"side-by-side (並行した)" と言うのです)。

プライベート DLL の実装には、2 つのアプローチがあります。新しいアプリケーションやコンポーネントを作成する場合は、各バージョンに一意なバージョン番号を付けます。それから、各 DLL やコンポーネントをプライベート コピーが必要なアプリケーションのディレクトリに登録します。アプリケーション中のバージョン情報により、アプリケーションは共有 DLL のプライベート コピーをロードすることを認知します。

2 番目の Side-by-Side アプローチは、既存のアプリケーションのために用意されたものです。C:\SuperApp\SuperApp.exe は既存のアプリケーションで、今後の DLL アップグレードによって影響を受けないように保護したいと考えているもの、またはサービス パックのアップグレードで動かなくなってしまったものと仮定します。SuperApp にプライベートにしたい DLL を \SuperApp へ単純にコピーして、このディレクトリに ".\SuperApp.exe.local" という空のファイルを作成します。これで、SuperApp が始動して .local ファイルを見つけると、標準のパスを探す前に現在のディレクトリで DLL と COM サーバーを探すようになります。サービス パックのアップグレードで、アプリケーションが動かなくなった場合には、.local ファイルと必要な古い DLL を含めたインストール プログラムを作成して、カスタマに提供します。

バージョン固有 (新しいアプリケーション用) および .local (古いアプリケーション用) の Side-by-Side アプローチの両方に、次の特徴があります。

  • アプリケーション ディレクトリに置かれた DLL は、ロード ライブラリのパスがハードコーディングされていても、システム DLL の代わりにロードされます。

  • HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs にリストされている 20 個の KnownDLL のディレクトリをリダイレクトすることはできません。そのほとんどは、クロス プロセス状態を維持しなければならないために、並行して実行することができないのです。たとえば、kernel32、user32、ole32 は、リダイレクトできません。それらが、複数プロセスにまたがって存在する状態 (カーネル オブジェクト、ウィンドウ ハンドラ、ローカル サーバーのためのランニング オブジェクト テーブル) であるためです。将来の OS リリースでは、この DLL のうちいくつかは実装され、並行して実行することが可能になり、KnownDLL リストは短くなるでしょう。

  • 潜在的な可能性としては、すべてのタイプの DLL Hell を修正します。既存のアプリケーションでは、どの DLL をプライベートにする必要があるかを決めなければならないでしょう。

プライベート DLL へのバージョン アプローチは、コンポーネントまたはアプリケーションの作成者によって実装されなければなりません。システム管理者やインストール プログラムは、空の .local ファイルを作成し、DLL をプライベートにすることができます。

タイプ III DLL Hell は、まれに新しいバグを含んだサービス パックのアップデートにより起こりますが、Windows 2000 の Service Pack Track (SPトラック) を選択したカスタマでは、事実上発生しません(Windows 2000 アップデートには、2 つのトラックがあります。バグ フィックスのみで新機能を含まない SP トラックと、バグ フィックスと新機能の両方を希望するカスタマ向けの Point Release Track です)。

Windows 2000 と Windows 98 Second Edition の両方で WFP と Side-by-Side DLL を実装しています。Windows NT 4.0 と Windows 95 を使用しているユーザーで、この保護機能を利用したい場合にはアップグレードが必要です。 『アプリケーションで共有する Side-by-Side コンポーネントの実装 』 で、プライベート DLL について詳細に説明していますのでご覧ください。

DLL Hell へのアタック

サポート コールの多くの割合が DLL Hell に関係するものであるため、私はツールを作成して、サポート エンジニアの代わりにコンピュータに負担のかかる分析をさせようと考えました。その結果できたものが、DLL Universal Problem Solver (DUPS) パッケージです。これについては、Microsoft Knowledge Base 記事 Q247957 『SAMPLE: Using the DUPS Package to Resolve DLL Compatibility Problems』(英語)で、完全なソース コードも含めて、十分に説明されています。

DUPS パッケージは、単一のコンピュータで使用することや、ネットワーク上の全 DLL の履歴を追跡するために使用することができます。これは、Windows 95、Windows 98、Windows 2000 で稼動し、一番簡単なモードでは、依存関係は何もありません。DUPS パッケージは 3 つの C++ ユーティリティと 1 つの Visual Basic ビューアから構成され、詳細は次のようになります。

C++ データ生成サーバー コンポーネント

  • Dlister システムのすべての DLL について、レコード名、バージョン、サイズ、チェック日のリストを作成します。この情報はテキスト ファイルかデータベース (Microsoft Access または SQL Server?) にログが記録されます。

  • Dcomp 2 つのテキスト ファイルにリストされたすべての DLL を比較し、その差異を書き込んだ第 3 のファイルを作成します。

  • Dtxt2DB Dlister と Dcomp で作成されたテキスト ファイルを読み込んで、DLLhell データベースに入れます。

  • DlgDtxt2DB Dtxt2DB のグラフィカル ユーザー インターフェイス (GUI) ベースのバージョンです。このユーティリティは、カスタマから送付される分析用の Dlister の出力テキスト ファイルをインポートしやすくするために作成されました。

ネットワーク上のすべての DLL を検査するためには、ネットワークの全マシンにから利用可能な共有ドライブに、Dlister と Dcomp のイメージをインストールします。任意の Windows NT マシンでコマンド プロンプトから、"at" ジョブを入力して DLL 検査をスケジュールします。私は、次のテキストを検査したい各 Windows NT マシンのコマンド ウィンドウに貼り付けています。

at 3:15 /every:M,T,W,Th,F \\manic2\\DllHell\Dlister.exe

上記の at ** コマンドは、毎就業日の午前 3 時 15 分に Dlister を実行するようにスケジューリングされています。1 日に 2 つ以上の製品をインストールする場合は、Dlister を手動で実行し、各製品を別々に検査することができます。私のテスト マシン (450 MHz PII) では、Text モードで Dlister が完了するのに約 90 秒かかります。

Dlister の動作

  1. スタートアップ時に、Dlister は既定の動作をカスタマイズするための初期化ファイル DLLhell.ini から属性を読み込みます。コンピュータ固有属性または一般属性を指定できます。たとえば、 connectionString ** 属性を指定すると、Dlister は直接データベースに接続し、Dcomp と Dtxt2DB は使用されません。属性は、先に言及した Knowledge Base 記事で説明されています。

  2. Dlister はそのコンピュータに関するすべての DLL 情報を含む出力ファイルを作成します。最初に実行したときに、Dlister は computerName_DLL.txt を作成します ("computerName" は、Dlister イメージによる、Microsoft Win32R の GetComputerName() 呼び出しから返される文字列です)。2 回目以降は、既存の computerName_DLL.txt ファイルが見つかるので、Dlister は computerName_DLL_new.txt という新しいファイルを作成します

  3. Dlister が完了すると、Dcomp プログラムが実行されます。Dcomp は、各ファイルにリストされたすべての DLL を比較します。DLL の変更はすべて "computerName_DLL_changes_DateTime.txt" というファイルに書き込まれます。DateTime は日付と時間の文字列であり、したがってファイルがユニークであることを保証します。このファイルを、変更ファイル (changes file) と呼んでいます(最初に実行したときは、比較のための computerName_DLL_new.txt が存在しないため、computerName_DLL.txt ファイルが変更ファイルにコピーされます)。

  4. Dcomp が完了すると、変更ファイルは DLLhell サーバーにコピーされます(サーバー ディレクトリ、Dlister と Dcomp の出力ディレクトリはすべて DLLhell.ini ファイルに指定することができます)。

  5. 変更ファイルがサーバーにコピーされた後、computerName_DLL_new.txt (もし存在すれば) は computerName_DLL.txt という名前に変わります。記録する変更がなければ、computerName_DLL_new.txt は削除されます。

  6. 新しいファイルが変更ディレクトリにコピーされると、DLLhell サーバーはその知らせを受けて、Dtxt2DB プログラムを実行します。Dtxt2DB は変更ファイルを読み込んで DLLhell データベースを更新します。その後、変更ファイルは削除されます。サーバーは、Win32 API ReadDirectoryChangesW を使って、処理すべき新しいファイルの出現を効率よく待つことができるようになっています。SDK サンプル Fwatch で、この API のデモを見ることができます。

ふうっ~!ただ DLL の変更を追跡するだけなのに、ステップがたくさんあって複雑ですね。もし Dlister への接続文字列があれば、ステップ 2 ~ 6 までを省くことができます。そうすれば、Dlister を実行するだけでよいのです。では、なぜ私が残りの面倒なステップを全部行っているのでしょうか?

平均的なワークステーションには約 1,200 の DLL があり、クライアントからサーバーへ 1,200 周も旅しなければならないことになります。その間サーバーは、DLL ごとに数 100 バイトのデータを返します。各クエリーは相当の負担となり、数台以上のマシンで Dlister を実行すると、SQL サーバーとネットワークが忙殺されてしまうでしょう。たいていのコンピュータでは、日ごとの DLL の変更はまったくないか、あってもわずかです。しかし、データベースに接続された Dlister を実行すると、DLL を 1 つ 1 つ調べて現在のバージョンと比較しなければならないのです。

DLL の変更をモニタするために分散クライアント/サーバーのアプローチをすることで、ほとんど大部分の作業がクライアント マシンで行われるようになり、ネットワーク トラフィックは微々たる程度にまで削減されます。これによっては、廃棄物となっていた古い 120 MHz Pentium が楽々と 50 のワークステーションを扱えるという、スケーラブルなアーキテクチャをもたらします。私の DLLhell データベースでは、モニタするワークステーションごとに約 1 MB を使っています。それには、各マシン上の全 DLL と先月分の DLL の変更がすべて含まれています。

DLLhell データベースのデータ参照

Dllview アプリケーションは Visual Basic で書かれており、Microsoft ActiveXR Data Objects (ADO) を使って DLLhell データベースにアクセスします。MDAC 2.1 (またはそれ以降) と、Access または SQL Server が必要となります。数台以上のコンピュータを検査するならば、SQL Server を強くお勧めします。SQL Server がない場合でも、あなたが Visual Studio ファミリのユーザーであればは、次の Web ページから無償でデスクトップ バージョン SQL Server 互換の MSDE をダウンロードすることができます。
http://msdn.microsoft.com/vstudio/msde/

Compare モードを選択すると、DLLhell データベース中の検査されたコンピュータのリストが表示されます。比較したい 2 台のコンピュータを選ぶと、ビューアは、一方のグリッド ボックスに異なる DLL の全リスト、もう一方のボックスに等しい DLL の全リストを表示します。

[show details] を選ぶと、異なる DLL を 1 つずつリストし、各システムの DLL の名前、バージョン、日付、サイズを表示します。最もシンプルなシステムでさえも、非常に多くの DLL があるため、2 つのマシンのすべての DLL を比較しようとするとすぐに悲鳴を上げてしまうでしょう。ある特定のアプリケーションが 2 台のコンピュータで違った動きをするならば、そのアプリケーションが使う DLL だけを比較すればよいのです。 [File] メニューから [Compare List ...] を選んで、ダイアログ ボックスに従えば、そのアプリケーションがロードしたすべての DLL がリストされている .txt ファイルに行き着きます。ロードされた DLL をリストしているテキスト ファイルは、次のユーティリティのいずれかで作成できます。

図 1 は Detail+Limit モードでの Dllview を示しています。

ms811694.dlldanger01(ja-jp,MSDN.10).gif

図 1. DANKNT コンピュータと LPCHEHALIS3 コンピュータで比較した 31 個の DLL 中の 10 番目の DLL を表示している、DLLview アプリケーションの [Detailed View] ダイアログ ボックス

このビューで、SHELL32.DLL は 2 つのシステム間で異なっていることがわかり、また同時に Matching リスト ボックスには一致した 7 つの DLL が表示されます。

Time-Date モードで、コンピュータと、特定の時間または時間の範囲を選ぶと、ビューアは指定した時間に変更されたすべての DLL を表示します。このモードは、DLL Hell を引き起こしたインストールを突きとめる場合に便利です。

Duplicate モードでは、システム上の重複する DLL をすべて表示します。カスタマから送られてくる DLL リストのほとんどで、WINNT\$NtServicePackUninstall に非常に多くの重複 DLL が見られます。これは、サービス パックを完全に取り消すことができるように、たいてい Windows NT SP オプションを選んで古い DLL を保存しているからです。Windows NT SP アップグレードは、最も堅牢なインストーラであると考えられていますが、アンインストール オプションを選択するときにはタイマーをセットして、1か月後に WINNT\$NtServicePackUninstall ディレクトリを削除してよいか問い合わせるようにするべきです。

きっと、自分のコンピュータにある重複 DLL の数の多さに驚くはずです。

DUPS がすでに成し遂げたこと

私は DUPS パッケージを使って 10数件ものカスタマの苦情を解決してきました。最も簡単に解決できる障害は、コンピュータによってアプリケーションが動いたり、動かなかったりする場合です。このために、私はカスタマに 64 KB の Dlister を送って、動くシステムと動かないシステムの両方で実行するように依頼します。この結果、各マシンの全 DLL をリストした 2 つのテキスト ファイル (通常、各 250 KB) が生成されます。問題をはらむアプリケーションで使われる DLL だけに限定して比較を行うため、先にリストしたユーティリティの 1 つを使って、そのアプリケーションでロードされる DLL をリストしたテキスト ファイルも送ってもらいます。

私は 3 つのテキスト ファイルを受け取ると、良い DLL と悪い DLL のリストを DLLhell データベースにインポートし、DLLviewer を使って各 DLL を比較します。約 1 分で、差異のある DLL を報告することができます。

このアプローチにおいて一番の悩みの種は、カスタマから DLL データを受け取って、5 分後に次の結果を得たときのことです。 「動かないコンピュータは abc.dll バージョン n.2 を使用し、動くコンピュータは abc.dll バージョン n.1 を使用しています。」 このすぐ後、私は熱烈な電子メールを受け取り、そこにはお礼とあわせて次のように書かれています。「動くコンピュータから動かないコンピュータに abc.dll をコピーしたら、問題が解決しました。」ここで、私はカスタマに電話をして、DLL は独立したものではなく、セットになっているのだと説明しなければなりません。DLL の不完全なセットをインストールすると、現在の問題は修正されても、別のアプリケーションに問題が起きる可能性があります。その上、サポートされないシステム構成になってしまっているのです。私が悲観的にそう伝えると、たいてい次の答えが返ってきます。「オーケー、時間ができたら直すよ。」ほとんどのプログラマにとって、「時間ができたら」という表現は、結局 2 週間後には「絶対やらない」ということとなってしまうのです。

制約

DUPS パッケージは、同一のイメージが別のコンピュータで同じように実行されないという矛盾を、すべて解決できるわけではありません。2 台のコンピュータがまったく同じソフトウェアをインストールしている場合ですら、違った動きを示すことがあります。矛盾が起きる理由の 1 つは、ハードウェアが異なるためだと考えられます。実際には、異なるハードウェアが両方とも INTEL プロセッサを使っていれば、これはめったにありえません。そのような場合でも、プログラムが異なった動きをする理由としては、デバイス ドライバのバグの可能性があります。

スレッドの同時実行バグのせいで、同一ソフトウェアを持つ 2 つのマシンが違った動きをする場合がよくあります。私は、バグがスレッドの同時実行に起因しているのではないかと考えたときは、いつでもすぐに 2 あるいは 4 プロセッサ マシンでテスト プログラムを実行します。マルチスレッド アプリケーションのスレッド同時実行バグは、シングル プロセッサ マシンで再現するのは非常に難しいのですが、マルチプロセッサ システムではずっと再現しやすいからです。

マシン間でアプリケーションが矛盾する別の原因としてありがちなことは、アプリケーションが永続データを読み込むときに起こります。データは .ini ファイル、データベース、またはレジストリの場合があります。通常データベースと .ini ファイルは、容易に比較できますが、レジストリ データを比較するのは非常に困難です。レジストリに問題があるのではないかと考えたときには、regmon ユーティリティ (http://www.sysinternals.com/ から無償で入手可能) を実行して、各システムのアプリケーションで読み込まれるレジストリ値を比較します。何百ものレジストリ値を比較するのは一般の人にとっては大変なことなので (そして、私のような難読症の人間には事実上不可能なので)、次に作成するツールは RegistryHell ユーティリティにしなければと思っています。

また、タイミングもプログラムの結果に影響を及ぼし、同一のソフトウェアがインストールされたシステムでプログラムが異なる動きをするという、この問題の解決を困難あるいは不可能にしています。たいていのプログラマは、動かないプログラムをデバッガのもとで実行すると何故か正しく動作するというような、この現象を体験しています。また、タイミングの変化のために、ヒープの破損や初期化されていない変数の使用が表面化しない場合もあります。私は今後の記事で、ヒープ破損を発見するための最高のツールである、PageHeap について扱うつもりです。

DLL Hell は、ほぼ間違いなく Microsoft が直面している最大の問題です。Windows 2000 では、プライベート DLL と WFP によってこの問題の解決に取り組んでいます。WFP とプライベート DLL のベータ版のテスタは、これらのテクノロジを圧倒的に支持しています。プライベート DLL により、Windows 2000 は、共有リソースの強みと、本質的に静的なイメージを作成するフレキシビリティを持つことになります。Windows 2000 は、このフレキシビリティのおかげで UNIX より有利な立場に立っています。

将来の方向性

NTFS を使う Windows 2000 システムに対しては、Dlister2 が NTFS Change Journal を読み込み、最後に Dlister2 が実行されてから今までに起きたすべての DLL の変更を探し出すようにする予定です。NTFS Change Journal はすべてのファイル変更をログしているので、Change Journal を使用する Dlister では、現在数分かかっている全ディスクのスキャンを、数秒で完了してしまうでしょう。

DLL Hell は本当に終わったのか?

WFP とプライベート DLL により堅牢さが飛躍的に増強されたことは、Windows 2000 が他のオペレーティング システムに優る点として、重要な意味を持ちます。静的リンクの堅牢さを保持しながら DLL のリソース節約が可能となったのです。DLL Hell は完全に消滅することはありませんが、発生する可能性はかなり低いといえます。問題をはらんだ DLL を見つけるために DUPS を、そして問題を修正するために Side-by-Side DLL を使えば、DLL のバージョン問題はもっと簡単に発見し修正できるようになるでしょう。

DUPS パッケージには、複雑なコードは 1 つもありません。ADO は COM オブジェクトから構成されているため、低水準の、あるいはパフォーマンス クリティカルな C++ ルーチンや Visual Basic GUI プログラムで、同じデータベース API を使うことができます。Visual Basic ビューアが C++ ですでに作成されている特殊な機能を必要とした場合には (DLL データ テキスト ファイルを読み込んで解析するなど)、私は ATL の既存の C++ コードをラップすることで、それらの公開されたメソッドを Visual Basic から呼び出すことができたのです。