Google Chrome をデバッグする (1)

Google Chrome のデイリーリリースバージョンは,炭鉱にて危険をいち早く察知するカナリアにたとえられ Canary 版と呼ばれている.
(炭鉱で戦うものたちの熱い物語については『炭鉱の庭師』を参照されたい)
Google Chrome Canary 版は以下のページからインストールできる.
https://tools.google.com/dlpage/chromesxs
Canary 版と安定版は,インストール先フォルダから使用するプロファイルまで異なる別アプリケーションである (Side-by-side インストールと呼ばれている).両者を同時に起動することももちろん可能だ.また,Canary 版であっても Google Update の対象になることから,一度インストールしてしまえば後は自動で trunk を追いかけてくれる.
実は,Canary 版にはもうひとつ大きな特徴がある.それは,ビルドの副産物であるデバッグシンボルが公開されていることだ.ここで,デバッグシンボルが公開されていることで何が可能になるかについて,数回にわたって紹介してみたい.

シンボルサーバの登録

Windows エコシステムでのシンボルサーバの登録には,環境変数 _NT_SYMBOL_PATH を使用するのが一般的だ.Microsoft のシンボルサーバと Chrome のシンボルサーバを登録する場合,_NT_SYMBOL_PATH は次のように設定することになる.

_NT_SYMBOL_PATH=srv*c:\SymCache*http://msdl.microsoft.com/download/symbols;srv*C:\SymCache*http://chromium-browser-symsrv.commondatastorage.googleapis.com

"C:\SymCache" は別の名前に変更することも可能である.このフォルダにダウンロードされたシンボルデータがキャッシュされるので,なるべく高速なドライブを設定すると良い.
次に,よく利用する Sysinternals のツール群についてもシンボルパスを設定する.
f:id:NyaRuRu:20120930014804p:plain
Process Explorer と Process Monitor それぞれで,Options から Configure Symbols を選択する.Dbghelp.dll path については,Windows SDK 付属のものを選ぶとよい.64-bit 環境では x64 版の Dbghelp.dll を選択しよう.Symbol paths については先ほど同様に以下を設定する.

srv*c:\SymCache*http://msdl.microsoft.com/download/symbols;srv*C:\SymCache*http://chromium-browser-symsrv.commondatastorage.googleapis.com

Process Explorer を利用したハングアップ解析

Google Chrome Canary 版が無反応になったなら,Process Explorer を起動してみよう.デバッグシンボルが完備されていればその場でスレッドのコールスタックを見るのも容易である.(ただし,ここで最も慎重な次の一手はプロセスダンプをとることだ)
状況の保全のため,プロセスの全スレッドを中断させてみよう.一般的に,Chrome のウィンドウが無反応になったときには Chrome のブラウザプロセスの問題である.Process Explorer の "Process" 欄を何度かクリックして,表示モードをプロセスツリーに変更しよう.ここでルートに当たる chrome プロセスがブラウザプロセスである.(別の識別法として,プロセスの起動引数に --type オプションが付いていないプロセスを選ぶという方法もある)
f:id:NyaRuRu:20120930014912p:plain
ブラウザプロセスを見つけたら,コンテキストメニューから Suspend を選択する.これでブラウザプロセスの動作が中断する.動作を中断させたら,コンテキストメニューから Properties を選択し,Threads タブに移動する.
f:id:NyaRuRu:20120930014932p:plain
ここでスレッドを選択し Stack ボタンをクリックすると,該当スレッドのコールスタックが表示される.
f:id:NyaRuRu:20120930014952p:plain
コールスタックを表示すると,非同期でシンボルのダウンロードが始まる.しばらくすると,Chrome のどういった関数が呼び出されていたのかが表示されるだろう.UI スレッドを探し出し,そのコールスタックを探し当てたら,それがハングアップの現場である.

Process Monitor を利用した I/O モニタリング

Process Monitor を利用することで,以下のような動作のモニタリングが可能になる.

  • 任意のファイルアクセス
  • 任意のレジストリアクセス
  • 任意のプロセス起動/スレッドイベント/DLL のロードとアンロード
  • 任意のネットワークアクセス

シンボルがセットされていると,これらの各イベント発生時のコールスタックが取得できるようになる.また,Tools メニューの各 Summary 項目から,選択したイベントの集計やクロス集計も可能である.例えば,プロセス起動時から終了時までの間にあるファイルへ行われた全てのファイルアクセスについて,コールスタックごとにその割合を分析するといったことが可能だ.

f:id:NyaRuRu:20120930015009p:plain
Chrome を起動してあるページをブラウズし,終了するまでの間に行われた約 2000 回のファイル読み取りアクセスを,コールスタックを用いてブレイクダウンしている図

Process Monitor を用いた解析テクニックについては,例えば次の記事などが参考になる.

次回,API モニタ編に続く.

SSD なら動作を変えるアプリケーションを作る

Windows 7 以降の OS では,SSD 上のボリュームに対してデフラグがスケジューリングされません.これと同様に,ドライブ特性に応じた動作戦略の変更をアプリケーションでも行いたいこともあるでしょう.そのような場合にアプリケーションに組み込めるように,簡単なサンプルコードを書いてみます.
元ネタは,
Windows 7 Disk Defragmenter User Interface Overview - The Storage Team at Microsoft - File Cabinet Blog - Site Home - TechNet Blogs
で解説されているアルゴリズムです.

以下,物理ドライブ "\\.\PhysicalDrive0" について,"no seek penalty" かどうかと,"nominal media rotation rate" かどうかの取得を行います.なお,前者はディスクポートドライバの動作に依存するため,Windows 7 以降でなければ使用できませんが,管理者権限なしで動作するというメリットがあります.後者は ATA8-ACS に対応したデバイスであれば,Windows 7 より前の OS でも動作しますが,動作には管理者権限が必要です.
上記記事に解説されているように,これらの関数を組み合わせて使ってもよいですし,どちらか一方のみを使いある程度の false negative は諦めるという手もあります.

#include <Windows.h>
#include <WinIoCtl.h>
#include <Ntddscsi.h>
#include <Setupapi.h>

#include <iostream>
#include <string>

using std::wstring;
using std::wcout;
using std::endl;

// Returns S_OK if |physical_drive_path| has no seek penalty.
// Returns S_FALSE otherwise.
// Returns E_FAIL if fails to retrieve the status.
// |physical_drive_path| should be something like
// "\\\\.\\PhysicalDrive0".
HRESULT HasNoSeekPenalty(const wstring& physical_drive_path) {
    // We do not need write permission.
  const HANDLE handle = ::CreateFileW(
      physical_drive_path.c_str(), FILE_READ_ATTRIBUTES, 
      FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
      OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (handle == INVALID_HANDLE_VALUE) {
    return E_FAIL;
  }

  STORAGE_PROPERTY_QUERY query_seek_penalty = {
    StorageDeviceSeekPenaltyProperty,  // PropertyId
    PropertyStandardQuery,             // QueryType,
  };
  DEVICE_SEEK_PENALTY_DESCRIPTOR query_seek_penalty_desc = {};
  DWORD returned_query_seek_penalty_size = 0;
  const BOOL query_seek_penalty_result = DeviceIoControl(
    handle, IOCTL_STORAGE_QUERY_PROPERTY,
    &query_seek_penalty, sizeof(query_seek_penalty),
    &query_seek_penalty_desc,
    sizeof(query_seek_penalty_desc),
    &returned_query_seek_penalty_size, NULL);
  CloseHandle(handle);
  if (!query_seek_penalty_result) {
    // failed to retrieve data.
    return E_FAIL;
  }

  return !query_seek_penalty_desc.IncursSeekPenalty ?
      S_OK : S_FALSE;
}

// Returns S_OK if |physical_drive_path| has nominal media
// rotation rate in terms of ATA8-ACS specification.
// http://www.t13.org/Documents/UploadedDocuments/docs2007/D1699r4-ATA8-ACS.pdf#Page=179
// Returns S_FALSE otherwise.
// Returns E_FAIL if fails to retrieve the status.
// |physical_drive_path| should be something like
// "\\\\.\\PhysicalDrive0".
HRESULT HasNominalMediaRotationRate(
    const wstring& physical_drive_path) {
  // In order to use IOCTL_ATA_PASS_THROUGH,
  // We *do* need read/write permission, which means
  // that the caller has admin privilege.
  const HANDLE handle = CreateFileW(
    physical_drive_path.c_str(),
    GENERIC_READ | GENERIC_WRITE, 
    FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
    OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (handle == INVALID_HANDLE_VALUE) {
    return E_FAIL;
  }

  struct ATAIdentifyDeviceQuery {
    ATA_PASS_THROUGH_EX header;
    WORD data[256];
  };

  ATAIdentifyDeviceQuery id_query = {};
  id_query.header.Length = sizeof(id_query.header);
  id_query.header.AtaFlags = ATA_FLAGS_DATA_IN;
  id_query.header.DataTransferLength =
      sizeof(id_query.data);
  id_query.header.TimeOutValue = 3;  // sec
  id_query.header.DataBufferOffset =
      sizeof(id_query.header);
  id_query.header.CurrentTaskFile[6] =
      0xec;  // ATA IDENTIFY DEVICE command

  DWORD retval_size = 0;
  const BOOL result = DeviceIoControl( 
    handle, IOCTL_ATA_PASS_THROUGH,
    &id_query, id_query.header.DataTransferLength,
    &id_query, id_query.header.DataTransferLength,
    &retval_size, NULL);
  if (!result) {
    return E_FAIL;
  }
  const int kNominalMediaRotRateWordIndex = 217;
  // RPM == 1 means this is non-rotate device
  return id_query.data[kNominalMediaRotRateWordIndex] == 1 ?
      S_OK : S_FALSE;
}

int main() {
  const wstring kDrive = L"\\\\.\\PhysicalDrive0";

  switch (HasNoSeekPenalty(kDrive)) {
    case S_OK:
      wcout << kDrive << L" has no seek penalty." << endl;
      break;
    case S_FALSE:
      wcout << kDrive << L" has seek penalty." << endl;
      break;
    default:
      wcout << L"failed to retrieve the status." << endl;
      break;
  }
 
  switch (HasNominalMediaRotationRate(kDrive)) {
    case S_OK:
      wcout << kDrive << L" has no seek penalty." << endl;
      break;
    case S_FALSE:
      wcout << kDrive << L" has seek penalty." << endl;
      break;
    default:
      wcout << L"failed to retrieve the status." << endl;
      break;
  } 

  return 0;
}

さて,"C:\Windows\System32\calc.exe" のようなパス名から,そのファイルが存在する物理ドライブ名を取得するにはどうすればよいでしょうか?
これには,

  1. GetVolumePathName API を利用してパスからマウントポイントを取得する
  2. GetVolumeNameForVolumeMountPoint API を利用して,論理ボリューム名を取得する
  3. IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS IO Control を利用して,物理ドライブの番号を取得する

という手順を踏みます.後は,得られた番号を "\\.\PhysicalDrive" の末尾に付ければ,物理ボリューム名が判明します.
なお,ダイナミックボリュームでは,複数の物理ドライブにまたがった論理ボリュームが作成可能なことに注意してください.以下の関数は,指定されたパスに対応する物理ドライブ番号を返します.

vector<int> GetExtentsFromPath(const wstring& path) {
  vector<int> extents;

  wchar_t mount_point[1024];
  if (!GetVolumePathNameW(
        path.c_str(), mount_point, ARRAYSIZE(mount_point))) {
    return extents;
  }

  wchar_t volume_name[1024];
  if (!GetVolumeNameForVolumeMountPointW(
          mount_point, volume_name, ARRAYSIZE(volume_name))) {
    return extents;
  }

  wstring volume = volume_name;

  // remove trailing '\\'
  volume.resize(volume.size() - 1);

  // We do not need write permission (nor admin rights).
  const HANDLE volume_handle = CreateFileW(
      volume.c_str(), FILE_READ_ATTRIBUTES, 
      FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
      FILE_ATTRIBUTE_NORMAL, NULL);

  VOLUME_DISK_EXTENTS initial_buffer = {};
  DWORD returned_size = 0;
  const BOOL get_volume_disk_result = DeviceIoControl(
      volume_handle, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
      NULL, 0, &initial_buffer, sizeof(initial_buffer),
      &returned_size, NULL);
  const DWORD query_size_error = GetLastError();
  if (get_volume_disk_result != FALSE &&
      initial_buffer.NumberOfDiskExtents == 1) {
    extents.push_back(initial_buffer.Extents[0].DiskNumber);
    return extents;
  }
  if (query_size_error != ERROR_MORE_DATA) {
    return extents;
  }

  const size_t buffer_size =
      sizeof(initial_buffer.NumberOfDiskExtents) + 
      sizeof(initial_buffer.Extents) *
      initial_buffer.NumberOfDiskExtents;
  char* underlaying_buffer = new char[buffer_size];
  VOLUME_DISK_EXTENTS* query_buffer =
      reinterpret_cast<VOLUME_DISK_EXTENTS *>(
          &underlaying_buffer[0]);
  const BOOL devide_ioc_result = DeviceIoControl(
      volume_handle, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
      NULL, 0, 
      query_buffer, buffer_size, &returned_size, NULL);
  const DWORD device_detail_result_error = ::GetLastError();
  if (!!devide_ioc_result) {
    for (DWORD i = 0;
         i < query_buffer->NumberOfDiskExtents; ++i) {
      extents.push_back(query_buffer->Extents[i].DiskNumber);
    }
  }
  delete[] underlaying_buffer;
  CloseHandle(volume_handle);
  return extents;
}

幸いなことに,上記処理は管理者権限なしでも動作します.Windows 7 以降では,管理者に昇格する必要なしに,SSD 上で動作しているらしいかどうかをアプリケーションが判別できるということになります.

Metro スタイルアプリケーションと IME

Metro では,AppContainer という特殊な環境でアプリケーションが実行されます.IME DLL は *1,対象の Metro スタイルアプリケーションプロセスに読み込まれ,AppContainer の管理下で動作することが求められます.

実際,Microsoft は Windows 8 Release Preview の公開に合わせ,Guidelines and checklist for IME development (Metro style apps) というガイドラインの提供を開始しました*2.同ドキュメントには,AppContainer 内で IME の機能を実装する上で,次のようなケースに注意せよとあります.

  • 辞書ファイルの置き場所
  • インターネットを利用したアップデート
  • 学習機能
  • プロセス間での(設定や学習)情報の共有

同ドキュメントには,IME の学習機能や,プロセス間での(設定や学習)情報の共有機能の実装方法として,その AppContainer で可能であればウェブサービスを経由してこれらの機能を実装するよう書かれています.Mac や Android のように IME プロセスを分離してくれていれば,IME 単体に別のアクセスコントロールを適用することも可能だったのでしょうが……

なお,Metro アプリケーションとデスクトップアプリケーションでは,利用可能なテクノロジに違いがあります.詳細については API reference for Metro style apps から辿ることができます.Metro 対応 IME では IMM32 ではなく TSF を使う必要があることはガイドラインで明記されていましたが,その他の API についてははっきりとは書かれていません.公開されると言われている Metro 対応 IME サンプルでも読まないとはっきりしない予感がします.
なお,こうして作られた IME DLL は,従来のデスクトップアプリケーションに読み込まれても動作する必要があります.まさに Run Anywhere,Universal Binary というわけです.

*1:Metro スタイルアプリケーションは,TSF のみのサポートとなりますから,IME というより Text Input Processor; TIP DLL と呼ぶ方が適切かもしれませんが

*2:一方で,予定されていた Metro アプリ用 IME サンプルに関する情報は[http://social.msdn.microsoft.com/Forums/en-US/winappswithnativecode/thread/6abc8b91-110c-4d60-b5b7-e113144902d9:title=削除されました].

Chromium Sandbox を用いた保護モード

Chromium Sandbox は,乱暴に言えば

  1. 対象プロセスの権限や動作をとにかく制限する
  2. それだと目的の動作もできなくなることがあるので,本当に必要な処理についてはより強い権限を持ったプロセスで代理処理する

という二段構成になっています.

2. の代理処理は,Sandbox 化されたプロセス内での API 呼び出しをフックして行います*1.悪意のあるコードが API フックを回避し,本来のシステムコールに到達した場合,プロセス本来のアクセス権限によるチェックが行われます.このケースまで想定して,前者の「とにかく」のレベルが決まります.

1. の制限は強ければ強いほど良いため,Chromium Sandbox では,先ほど出てきた整合性レベルに加え,プロセスのアクセストークンからの権限削除,Job を利用した制約,デスクトップ分離などさまざまなテクニックが使われています.実際には,アプリケーション本来の目的を阻害するような制約は諦めざるをえないため,アプリケーションごとにどの制約を組み合わせるかを選択します.Adobe Reader のケースでは,一部の設定について PC 管理者がカスタマイズすることを許可しています.

Adobe Reader に関しては,次の文献などで詳細な解析が行われています.
http://media.blackhat.com/bh-us-11/Sabanal/BH_US_11_SabanalYason_Readerx_WP.pdf

Chromium Sandbox のような特殊な環境で IME が動作しているとき,正常に動作しているかどうかを判断するのは難しいものがあります.多くの場合,プロセスを起動するような操作しばしば失敗します.これは,辞書ツールや文字パレットが別プロセスとして実装されているケースでは明らかになるでしょうが,これらのツールが対象プロセス内に表示されるウィンドウとして実装されている場合には表面化しないかもしれません.IME の確定履歴がディスク上に書き込めないというケースもあります.Process Monitor でのログを見るだけで問題が明らかになることもありますが,いずれにせよ IME についての理解は必要です.

*1:AdHoc なルールを透過的に適応するという点では Windows の互換性スイッチに似ているとも言えます

整合性レベルに基づく保護モード

整合性レベル導入による互換性上の影響は,大まかに次の 2 つに分けることができます.
1. アクセス権不足によるリソースアクセスの失敗
2. ユーザーインターフェイス特権の分離 (UIPI) による,プロセス間通信の失敗
前者について,整合性レベルは OS 標準の ACL の上に実装されており,その実装についても詳細なドキュメントが存在します.
後者については,主にウィンドウシステムとウィンドウ間の通信に関する仕様変更で,こちらも詳細にドキュメント化されています.

互換性上の問題に直面したとき,開発者は,整合性レベル Low のプロセス用に専用の設定ファイルを用意したり,カーネルオブジェクトの ACL を変更して整合性レベル Low のプロセスからもオープンできるようにしたり,UIPI の例外を登録したりといった回避策をとることができます.

保護モードと IME

Windows アプリケーションに,統一的な "Protected Mode" (保護モード)の仕様や基準といったものはありません.保護モードと呼ばれている各技術の実装は製品ごとに異なります.A という製品の「保護モード」を有効にした状態で IME が動く(ように見える)からといって,B という製品の「保護モード」を有効にしても同じ IME が動く(ように見える)とは限りません.

例えば,Google Chrome は,ウィンドウ処理を行うブラウザプロセスと,HTML のレンダリングを行うレンダラプロセスを分けており,Chrome の保護モードが適応されるのはレンダラプロセスの方です.IME はウィンドウ処理が行われるブラウザプロセスに読み込まれるため,IME にとっての Chrome は,(プラグイン周りをいったん忘れることにすると) メモ帳などと同じただのデスクトップアプリケーションです.

UAC が有効な環境での Microsoft Internet Explorer は,整合性レベルが Low に設定されたプロセスでウィンドウ処理と HTML のレンダリングを実行します.IME はウィンドウ処理を行うプロセス内に読み込まれるので,IME は整合性レベル Low のプロセスで動作する必要があります.なお,Internet Explorer は,整合性レベルを Low にする以外ほとんどプロセスの設定を変更しないため*1,メモ帳を PsExec の -l オプションで起動して IME のテストをするだけでも十分有用です.

Adobe Reader や Firefox 版 Adobe Flash Player の「保護モード」はというと,Chrome の Sandbox 関連の技術を利用していることが Adobe から公表されています*2.Chrome とは異なるのは,ウィンドウ処理を行うプロセス自体を Sandbox 内で実行するということです.このケースでは,IME は Sandbox 化されたプロセス内で動作することになります.

アプリケーション IME の動作環境
Google Chrome メモ帳等と変わらず
Microsoft Internet Explorer 整合性レベル Low
Adobe Reader X Sandboxed process

なお,ブラウザプラグイン特有の IME 関連の問題についてに関しては (基本的に) 今回は扱いません.

*1:他には,HKCU のマッピング が行われたり

*2:たとえば http://blogs.adobe.com/asset/tag/sandbox 内にもいくつか言及が見られます

IWordBreaker とファイル検索

「『プリキュア』で検索したら『ハートキャッチプリキュア』にマッチしない」という Windows Search の話.

Windows7に深刻なバグを発見したので、警鐘を鳴らすために晒してみます。
再現に使用したOSはWindows7 Home Premium x64です。

バグの再現手順

 
!!! 悪用厳禁 !!!
 
●1.適当にフォルダを作る 名前は何でもOK


 
●2.作ったフォルダーを開いて、
ハートキャッチプリキュア
ふたりはプリキュア
プリキュア
の3つのフォルダを新規作成する

 

●3.検索窓に「プリキュア」と入力してみる


 

●4.「ハートキャッチプリキュア」が無かったことにされる

ちくしょう!誰がこんなことを!メディーック!!メディーーーーック!!

対処方法

検索窓に「*プリキュア」と入れると全部ヒットするみたい。

でも、XPの頃は「プリキュア」で全部ヒットしてたのでなんか腑に落ちないアレが。

ちなみに検索インデックスの有無は関係ないみたいです。

#2010/10/30 11:05 追記
VistaやMacOSでも再現するとか。
Windowsの人は、「Everything」を使うと幸せになれるらしいです。

「従来何も考えずにファイル名の部分文字列で検索できていたのものを,どうしてアスタリスクが必要にしちゃったの?」という方向の話のような気もしますが,その辺は置いておいて久しぶりに IWordBreaker とか.
Windows 7 に標準で付いてくる日本語向け IWordBreaker 実装に「ハートキャッチプリキュア」等を食わせてみます.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
using Microsoft.Win32;
using WordBreaker;

namespace WordBreakerTest
{
  using HRESULT = System.UInt32;
  public struct HResults
  {
    public const HRESULT S_OK = 0x00000000;
    public const HRESULT S_FALSE = 0x00000001;
    public const HRESULT E_FAIL = 0x80004005;
    public const HRESULT WBREAK_E_END_OF_TEXT = 0x80041780;
    public const HRESULT LANGUAGE_S_LARGE_WORD = 0x00041781;
    public const HRESULT WBREAK_E_QUERY_ONLY = 0x80041782;
    public const HRESULT WBREAK_E_BUFFER_TOO_SMALL = 0x80041783;
    public const HRESULT LANGUAGE_E_DATABASE_NOT_FOUND = 0x80041784;
    public const HRESULT WBREAK_E_INIT_FAILED = 0x80041785;
  }

  public enum WORDREP_BREAK_TYPE
  {
    WORDREP_BREAK_EOW = 0,
    WORDREP_BREAK_EOS = 1,
    WORDREP_BREAK_EOP = 2,
    WORDREP_BREAK_EOC = 3
  }

  [SuppressUnmanagedCodeSecurity]
  [ComImport, Guid("CC907054-C058-101A-B554-08002B33B0E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  public interface IWordSink
  {
    [PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    HRESULT PutWord(
        uint cwc,
        [In][MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0, ArraySubType = UnmanagedType.U2)] char[] pwcInBuf,
        uint cwcSrcLen,
        uint cwcSrcPos);
    [PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    HRESULT PutAltWord(
        uint cwc,
        [In][MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0, ArraySubType = UnmanagedType.U2)] char[] pwcInBuf,
        uint cwcSrcLen,
        uint cwcSrcPos);
    [PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    HRESULT StartAltPhrase();
    [PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    HRESULT EndAltPhrase();
    [PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    HRESULT PutBreak(WORDREP_BREAK_TYPE breakType);
  }

  [SuppressUnmanagedCodeSecurity]
  [ComImport, Guid("CC906FF0-C058-101A-B554-08002B33B0E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  public interface IPhraseSink
  {
    [Obsolete("Not supported.")]
    [PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    HRESULT PutSmallPhrase(
        [In][MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1, ArraySubType = UnmanagedType.U2)] char[] pwcNoun,
        uint cwcNoun,
        [In][MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3, ArraySubType = UnmanagedType.U2)] char[] pwcModifier,
        uint cwcModifier, uint ulAttachmentType);
    [PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    HRESULT PutPhrase(
        [In][MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1, ArraySubType = UnmanagedType.U2)] char[] pwcPhrase,
        uint cwcPhrase);
  }

  public class WordSink : IWordSink
  {
    public Action<string, uint, uint> OnWord { get; set; }
    public Action<string, uint, uint> OnAltWord { get; set; }
    public Action<WORDREP_BREAK_TYPE> OnBreak { get; set; }
    #region CWordSink Members
    public HRESULT PutWord(
        uint cwc,
        [In][MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0, ArraySubType = UnmanagedType.U2)] char[] pwcInBuf,
        uint cwcSrcLen,
        uint cwcSrcPos)
    {
      if (OnWord != null)
      {
        OnWord(new string(pwcInBuf), cwcSrcLen, cwcSrcPos);
      }
      return HResults.S_OK;
    }
    public HRESULT PutAltWord(
        uint cwc,
        [In][MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0, ArraySubType = UnmanagedType.U2)] char[] pwcInBuf,
        uint cwcSrcLen,
        uint cwcSrcPos)
    {
      if (OnAltWord != null)
      {
        OnAltWord(new string(pwcInBuf), cwcSrcLen, cwcSrcPos);
      }
      return HResults.S_OK;
    }
    public HRESULT StartAltPhrase()
    {
      return HResults.S_OK;
    }
    public HRESULT EndAltPhrase()
    {
      return HResults.S_OK;
    }
    public HRESULT PutBreak(WORDREP_BREAK_TYPE breakType)
    {
      if (OnBreak != null)
      {
        OnBreak(breakType);
      }
      return HResults.S_OK;
    }
    #endregion
  }

  public class CPhraseSink : IPhraseSink
  {
    #region CPhraseSink Members
    public HRESULT PutSmallPhrase(
        [In][MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1, ArraySubType = UnmanagedType.U2)] char[] pwcNoun,
        uint cwcNoun,
        [In][MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3, ArraySubType = UnmanagedType.U2)] char[] pwcModifier,
        uint cwcModifier,
        uint ulAttachmentType)
    {
      return HResults.S_OK;
    }
    public HRESULT PutPhrase(
        [In][MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1, ArraySubType = UnmanagedType.U2)] char[] pwcPhrase,
        uint cwcPhrase)
    {
      return HResults.S_OK;
    }
    #endregion
  }

  [UnmanagedFunctionPointer(CallingConvention.StdCall)]
  public delegate uint FillTextBufferDelegate(ref TEXT_SOURCE pTextSource);

  [StructLayout(LayoutKind.Sequential)]
  public struct TEXT_SOURCE
  {
    [MarshalAs(UnmanagedType.FunctionPtr)]
    public FillTextBufferDelegate pfnFillTextBuffer;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string awcBuffer;
    public uint iEnd;
    public uint iCur;
  }

  [SuppressUnmanagedCodeSecurity]
  [ComImport, Guid("D53552C8-77E3-101A-B552-08002B33B0E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  public interface IWordBreaker
  {
    [PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    HRESULT Init(
        [MarshalAs(UnmanagedType.Bool)] bool fQuery,
        uint maxTokenSize, [MarshalAs(UnmanagedType.Bool)] out bool pfLicense);
    [PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    HRESULT BreakText(
        ref TEXT_SOURCE pTextSource, [MarshalAs(UnmanagedType.Interface)] IWordSink pWordSink,
        [MarshalAs(UnmanagedType.Interface)] IPhraseSink pPhraseSink);
    [PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    HRESULT GetLicenseToUse([MarshalAs(UnmanagedType.LPWStr)] out string ppwcsLicense);
  }

  public static class Program
  {
    public static void BreakText(string text, bool forQuery)
    {
      const string kWordBreakerKey =
          @"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ContentIndex\Language\Japanese_Default";
      var guid = new Guid(Registry.GetValue(kWordBreakerKey, @"WBreakerClass", string.Empty) as string);
      var wordBreakerType = Type.GetTypeFromCLSID(guid);

      // A newer wordbreaker shipped with MS Office 2010.
      // wordBreakerType = Type.GetTypeFromProgID("NLG.Japanese Wordbreaker.4.1");

      var wordBreaker = default(IWordBreaker);
      try
      {
        wordBreaker = Activator.CreateInstance(wordBreakerType) as IWordBreaker;

        var license = true;
        wordBreaker.Init(forQuery, 4096, out license);

        var filler = (FillTextBufferDelegate)((ref TEXT_SOURCE _) => HResults.WBREAK_E_END_OF_TEXT);
        var pTextSource = new TEXT_SOURCE()
        {
          pfnFillTextBuffer = filler,
          awcBuffer = text,
          iCur = 0,
          iEnd = checked((uint)text.Length),
        };

        var dictionary = new Dictionary<WORDREP_BREAK_TYPE, string>
        {
          {WORDREP_BREAK_TYPE.WORDREP_BREAK_EOC, "[EOC]"},
          {WORDREP_BREAK_TYPE.WORDREP_BREAK_EOP, "[EOP]"},
          {WORDREP_BREAK_TYPE.WORDREP_BREAK_EOS, "[EOS]"},
          {WORDREP_BREAK_TYPE.WORDREP_BREAK_EOW, "[EOW]"},
        };

        var words = new List<string>();
        var altWords = new List<string>();
        wordBreaker.BreakText(ref pTextSource, new WordSink
        {
          OnWord = (word, _, __) => words.Add(word),
          OnAltWord = (word, _, __) => altWords.Add(word),
          OnBreak = type => { words.Add(dictionary[type]); altWords.Add(dictionary[type]); },
        }, new CPhraseSink());
        GC.KeepAlive(filler);
        Console.WriteLine("Words: " + string.Join("/", words));
        Console.WriteLine("Alt Words: " + string.Join("/", altWords));
      }
      catch
      {
        if (wordBreaker != null)
        {
          Marshal.ReleaseComObject(wordBreaker);
          wordBreaker = null;
        }
      }
    }

    [MTAThread]
    static void Main(string[] args)
    {
      BreakText("プリキュア", false);
      BreakText("ふたりはプリキュア", false);
      BreakText("ハートキャッチプリキュア", false);
      BreakText("マイコンピューター", false);
      BreakText("情シス", false);
    }
  }
}
Words: プリキュア
Alt Words:
Words: ふたり/は/プリキュア
Alt Words:
Words: ハトキアッチプリキュア
Alt Words: ハートキャッチプリキュア
Words: マイコンピュタ
Alt Words: マイコンピューター
Words: 情/シス
Alt Words:

さすがに "プリキュア" で分割してくれたりはしないようですね.というかそもそも,「欧文地名以外の複合語をカタカナ表記するときは分かち書き」という Microsoft のスタイルガイド が遵守されているのが前提なのか,カタカナの連続は何も考えずにくっつけているだけのような挙動にも見えました.あんまりちゃんと実験してませんが.
ちなみに,SharePoint に付属する WordBreaker では,以下のようにユーザ辞書ファイルを使うことが出来るようです.

4. 以下に従い、ファイルを保存します。
場所 "C:\Program Files\Microsoft Office Servers\12.0\Bin"
(日本語ワードブレーカ nlsdata0011.dllが存在する場所)
ファイル名 "Custom0011.lex" (0011 は言語 ID)
文字コード "Unicode"

さらにこの nlsdata0011.dll というファイルですが,手元の Windows 7 Ja 環境では同名のファイルがシステムディレクトリに存在します.試しに %SystemRoot%\System32\Custom0011.lex (と %SystemRoot%\SysWOW64\Custom0011.lex) というファイルを作り,以下の内容を入力し,BOM 付き UTF-16 ファイルで保存してみます.

#CUSTOMER_WB
情シス
プリキュア

改めて最初のコードを実行すると,結果は以下のようになりました.

Words: プリキュア
Alt Words:
Words: ふたり/は/プリキュア
Alt Words:
Words: ハトキアッチプリキュア
Alt Words: ハートキャッチプリキュア
Words: マイコンピュタ
Alt Words: マイコンピューター
Words: 情シス
Alt Words:

少なくとも「情シス」の方は 1 word として認識されるようになりました.また,実行中に Custom0011.lex が読み込まれていることも,Process Monitor のログから確かめられました.
一方,ユーザ辞書に「プリキュア」を追加しても,"ハートキャッチ/プリキュア" と分割されませんでした.これは,以下の SharePoint での事例と同じもののようです.

ワードブレーキング (設定箇所 : サーバー定義ファイル)

こちらは、セミナーの資料では省略していましたが、懇親会でご質問がありましたので記載しておきます (懇親会場でご回答させて頂きました)。

例えば、「ペドロ&カプリシャス」のようなキーワードを検索したい場合、インデックス収集時に、間の記号(アンパサント &)によって、「ペドロ」と「カプリシャス」でキーワードが自動的に区切られます。こうした場合には、カスタムディクショナリー(Custom Dictionary) を設定することで、こうした自動ブレークを阻止し、「ペドロ&カプリシャス」で完全マッチの検索をおこなうことができます。

カスタムディクショナリの設定ファイルを配置する場所は、シソーラスファイルとは異なり、%programfiles%\Microsoft Office Servers\12\bin\CustomLANG.lex です。(日本語の場合は、Custom0011.lex です。) 設定を反映させるには、インデックスの再収集以外に、クエリー時のブレーク箇所も正しく認識させる必要があるため、ファイル編集後は、 Office SharePoint Server Search サービス (osearch) の再起動と、再クロールの双方をおこなってください。

カスタムディクショナリの作成方法については、以下の記事が参考になります。

TechNet : ユーザー辞書を作成する (Office SharePoint Server 2007)
http://technet.microsoft.com/ja-jp/library/cc263242.aspx

実は、懇親会では、「ワードブレークを阻止したい」 というご質問ではなく、逆に 「ワードを分割して認識させられないか」 というものでした。私は、この回答として、「カスタム辞書 (上記の CustomLANG.lex) を編集することで認識させられる可能性があるかもしれない」 とお答えしてしまいましたが、すみません、動作を確認してみたところ、本来分割されていないワードを分割して認識させることは不可能でした。(この予測は誤っておりました。申し訳ありません . . .)

発端の話も,「ワードを分割して認識させられないか」の一種だと思いますが,どうも現世代の Microsoft 製 IWordBreaker 実装ではユーザ辞書を使ってもこの問題を回避できなさそうな感じです. 次なる手段としては,自分で IWordBreaker を実装 して,HKLM\SYSTEM\CurrentControlSet\Control\ContentIndex\Language\Japanese_Default 以下の WBreakerClass を置き換えてしまう,あたりでしょうか.試してはいないので,うまくいくかは分かりませんが.