2009-11-13

Windows 7におけるプリコンパイルドヘッダーのエラーについて

Visual C++ Team Blog : Visual C++ Precompiled Header Errors on Windows 7

Visual C++コンパイラをWindows 7で使うと、以下のようなエラーが出ると、顧客が報告してきた。

fatal error C1859: 'stdafx.pch' unexpected precompiled header error, simply rerunning the compiler might fix this problem

このエラーは、以下のような条件を満たすと起きる。

  • Visual C++コンパイラがWindows 7上で実行される。
  • プリコンパイルドヘッダ(PCH)が有効になっている。
  • /analyzeが有効になっている(これは必ずしも必要条件ではないが、発生率を上げてしまう)

エラーメッセージの提案である、「単にもういちどコンパイルしてみる」は、おそらく問題を解決しないであろう。実際、この問題は、単なる問題ではなく、プリコンパイルドヘッダーの設計上の都合と、Windows 7における、新しいセキュリティ拡張の影響なのだ。

Visual C++ プリコンパイルドヘッダーとASLR

プリコンパイルドヘッダーファイルは、コンパイルされた「状態(state)」を保存しており、そのstate情報は、後のコンパイルに再利用され、ビルドのスループットを大幅に向上させるものである。過去15年に渡って、我が社のコンパイラは、プリコンパイルドヘッダーをディスクに書き込み、仮想メモリに直接リロードするという方法を用いていた。その信頼性は99.999%であり、パフォーマンスも十分に得られた。この方法の欠点は、設計がやや、難しいということだ。

というのも、PCHファイルには、ポインタが含まれている。つまり、仮想メモリ上で、作成された時とまったく同じアドレスにロードしなければならない。もしPCHが、後々のコンパイルで、別のアドレスに読み込まれてしまったならば、ポインタは不正なものになってしまう。さらにややこしい問題には、PCHはポリモーフィック・オブジェクトを含むのだ。ポリモーフィック・オブジェクトは、それぞれ仮想関数テーブルポインター(VFTP)を持っている。このVFTPは、モジュール内の仮想関数テーブルを指し示しているのである。そのため、もしPCH内のポリモーフィック・オブジェクトが、あるモジュールの仮想関数テーブルに依存している場合、そのモジュールは、PCHが作成された時と、まったく同じ仮想アドレスにロードしなければならない。もし、モジュールが、後々のコンパイルで、別のアドレスにロードされてしまったならば、PCH内のVFTPのは不正なものになってしまう。

以上が、「依存しあうPCHファイルとモジュールは、コンパイルごとにアドレスが移動してはならない」ということに関する、長々しい説明である。Visual C++コンパイラは、この状態が正しいかどうかを、ビルド前に確認し、上記のエラーを出して、失敗するのである。後者(訳注:PCHファイルないのVFTPが参照するモジュールのアドレス)が、Windows 7では問題になる。Windows 7は、より強力なアルゴリズムを用いたAddress Space Layout Randomization (ASLR)を提供しているのだ。ASLRは、プロセス内のモジュールをランダムにリロケートしてexploitを狙うマルウェアを防いでくれる。VistaのASLR randomizationに対応するために、Visudal Studio 2008のコンパイラのモジュールは、 /dynamicbase:noオプションをつけてビルドされていた。これは、ランダム化がより強力となったWin7では十分ではないのだ。

この問題への修正するために、我々はまず、コンパイラのモジュールのベースアドレスを、どこか「安全(モジュールの衝突の起こる確率が十分に低い)」な場所に固定しようと試みた。残念ながら、我々の努力にもかかわらず、モジュールは、安全なアドレススペースの別の場所に移動してしまい、それが積み重なって、結局はPCHで使用されるモジュールはリベースされてしまうのだ。このような失敗の原因は、特定するのが難しく、大抵は複数の要因が積み重なっている。プロセスの生成順序であるとか(つまり、devenv.exeはcl.exeの使うモジュールを読み込む等)、ネイティブDLLローダーの為だ。我々はついに、バタフライ効果との戦いに負けてしまった。

解決方法

代価案は、かなりの大きい作業量が伴うか、さもなければ、パフォーマンスがひどく落ち込むものであった。我々はついに、PCHデータ構造に、自前のディスパッチ機構を実装することによって、仮想関数テーブルを取り除いた。このPCHデータ構造の「非仮想化」によって、我々は二つ目の障碍も取り除くことに成功した。コンパイラのモジュールは、プリコンパイルドヘッダーファイルを壊さずに、プロセス内を自由に移動できるようになったのだ。

この修正はVisual Studio 2010の最終リリースに提供され、また、Visual Studio 2008への修正パッチも、追ってリリースされる見込みである。もし、この問題に遭遇してしまったならば、以下の方法を試して欲しい。

  • /analyzeが有効であれば、無効にする。
  • クリーンビルドする。
  • コンピューターをリブートする。
  • PCHファイルを無効にする。

No comments: