アジョブジ星通信

進捗が出た頃に更新されるブログ。

カーネルモードドライバーをそこそこデバッグできるようにした話

はじめての仮想HID」では、デバッグ手段が手に入れられなかったという終わり方をしてしまいましたが、それなりにデバッグできるようになったので、その方法をまとめておきます。

1. KdPrintEx でログを出力する

KdPrintEx マクロを使うと、デバッグビルド時(DBG 定数があるとき)に雑に Windows にログを残すことができます。 Microsoft から出ているサンプルでもよく使われているので、これで出力したログを見れる環境を作っていきます。

これを読んで理解できれば終わりですが……。

KdPrintEx の使い方

KdPrintEx はこのように使います。

KdPrintEx((ComponentId, Level, Format, フォーマットのための可変長引数));

マクロの都合上、括弧が二重になっています。

ComponentId には、どこで発生したログかを表す値を入れますが、サードパーティーのドライバーの場合は「DPFLTR_IHVDRIVER_ID」を指定しておくのが無難そうです。

Level はいろいろな指定の仕方がありますが、基本的には次の中から選べば良いと思います。

  • DPFLTR_ERROR_LEVEL
  • DPFLTR_WARNING_LEVEL
  • DPFLTR_TRACE_LEVEL
  • DPFLTR_INFO_LEVEL

Format および、その後に続く可変長引数には、 printf 形式でログの内容を指定することができます。最後に改行を入れておかないと、出力されるログも改行されないので、前後の出力がまとまってしまうことに注意してください。

以上を踏まえて、私はこのようなヘッダーファイルを作っておき、利用することにしました。カーネルモードでのログは「カーネルからのログ」という情報しかないので、ログの文字列の最初にプロジェクト名を入れておくことで、あとでフィルターをかけることができるようにしておきます。

#define TraceEnterFunc() KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "ProjectName: Enter %s, IRQL: %d\n", __FUNCTION__, KeGetCurrentIrql()))
#define TraceErrorStatus(funcName, status) KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "ProjectName: %s returned 0x%x in %s\n", funcName, status, __FUNCTION__))

#if DBG

#define TraceInfo(msg, ...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "ProjectName: " msg "\n", __VA_ARGS__)
#define TraceError(msg, ...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "ProjectName: " msg "\n", __VA_ARGS__)

#else

#define TraceInfo(msg, ...)
#define TraceError(msg, ...)

#endif

DebugView でログを見る

ログを見るのには、 Sysinternals の DebugView を使用すると便利です。

オプション次第(/v)ですべてのログを見ることができますが、それをやるとログの収集が追い付かず、ものすごく遅延が発生していくので、先に表示するログの設定をしておきます。

  1. レジストリHKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter キーを(なければ)作成します。
  2. ログの ComponentId に対応する DWORD 値を作成します。例えば DPFLTR_IHVDRIVER_ID なら「IHVDRIVER」という名前にします。
  3. その値を、表示させたいレベルのビットマスクにします。例えば、 INFO だけを表示(実際には値が 0 である ERROR も表示される)したいならば、 1 << DPFLTR_INFO_LEVEL(3) = 0x8 を値とします。 ERROR から INFO まですべてを表示させる場合は、すべてのビットを立てるので 0xF にします。(この範囲外の Level を指定した場合はまた話が変わってくるけれど割愛)

f:id:azyobuzin:20180809013052p:plain

準備ができたら、管理者権限で DebugView を起動し、 Capture Kernel を有効にするとログが見れるようになります。 Visual Studio から配置する場合は、 WDKRemoteUser 上で起動しておけば良いと思います。

f:id:azyobuzin:20180809014549p:plain

バグなのかと思いますが、一度 Capture Kernel を有効にして、 DebugView を終了させた後、再度 DebugView を起動すると、アクセス拒否エラーが出ます。再起動しないと直らなくてつらい。。

DebugView はリモートでも使用することができます。デバッグターゲットのマシンでは、管理者権限で「Dbgview /a /k」を起動します。 /a がエージェントモード、 /k がカーネルログの有効化です。ログを見る側のマシンでは、普通に DebugView を起動し、 Computer → Connect を選択して、 IP アドレスを入れると接続できます。こちら側でも Capture Kernel を有効にする操作が必要です。

2. KDNET でリモートカーネルデバッグ環境を構築する

以前の記事(はじめての仮想HID)では、 Visual Studio からプロビジョニングを行いましたが、そのときプロビジョニングは失敗していたはずです。しかし、次へをクリックすると「Status: Configured for driver testing」と表示されていたので成功したものだと思っていましたが、実際にはドライバーの配置のみが行える状態で、デバッグが出来ない状況でした。そこで、さらに調査を続けたところ、 Windows 10 では Visual Studio からのカーネルデバッグができなくなったようです。

This feature is not available in Windows 10, version 1507 and later versions of the WDK.

Setting Up Kernel-Mode Debugging over a Network Cable in Visual Studio | Microsoft Docs

そこで、とりあえず WinDbg 経由でリモートアクセスできる程度には環境を整えてみようということでやってみました。これを行うと Visual Studio からデバッグ実行を行ったとき、 Visual Studio 上から WinDbg を操作することができるようになりますが、 Visual Studio のエディタ上で設定したブレークポイントが反映されるようなことはありません。残念。

参考情報

これを読めば、この記事を読む必要がないリストです。

NIC の確認

リモートカーネルデバッグを行うには、対応している NIC を使用しなければいけません。つまり無線では無理ということっぽいです。

対応している NIC リストは次のページにあります。

今、テスト環境として使用しているマシンの、有線の NIC のプロパティはこのようになっています。

f:id:azyobuzin:20180809021138p:plain

赤丸がベンダーID、青丸がデバイスIDです。ベンダーID 1969、デバイスID 1091 はリストにあるので使えそうです。よかった。

次に、 NIC の場所をメモしておきます。

f:id:azyobuzin:20180809021411p:plain

この場合は、左から順に「3.0.0」と記録しておきます。これが後で使う Bus Parameter になります。

Visual Studio でプロビジョニング

せっかくなので、作業の半分くらいを Visual Studio にやってもらいます。どうせ Visual Studio から配置するわけですし。

Visual Studio で Configure Devices を開き、 Add New Device すると、勝手にポートやキーが設定されていると思います。ここで、 Bus Parameter の欄に先程調べた値を入力してプロビジョニングを行うと、この後の設定が完了した後から、 Visual Studio 上で WinDbg の操作を行うことができるようになります(その必要がないなら KDNET だけ設定しても良いです)。

f:id:azyobuzin:20180809021817p:plain

プロビジョニングには失敗しますが、デバイス一覧で「Status: Configured for driver testing」となっていれば完了です。

有線での設定を行いましたが、追加で、 WinDbg にアクセスできなくていいから、デプロイだけ無線でやりたいという場合は、さらに Add New Device して、プロビジョニングを行わず、無線 NIC 側の IP アドレスだけ指定して保存すれば、無線でデプロイできるようになります。この場合でも DebugView でログを見ながらデバッグできます(私はこの構成でやっています)。

KDNET でリモートカーネルデバッグの設定を行う

  1. C:\Program Files (x86)\Windows Kits\10\Debuggers\x64 から kdnet.exe と VerifiedNICList.xml を取ってきて、ターゲットマシンの C:\KDNET にコピーします。
  2. C:\KDNET 上で管理者としてターミナルを開き、 .\kdnet <ホストマシンのIPアドレス> <プロビジョニングで設定したポート番号> を実行します。
  3. 再起動すれば完了です。

もし、プロビジョニングを行っていない場合は、ポート番号は適当な値を使用し、実行結果として表示されるキーをメモしておいてください(bcdedit /dbgsettings でも確認できます)。

これで設定は完了です。あとは WinDbg でポートとキーを指定すれば、デバッグが開始されます。

f:id:azyobuzin:20180809024545p:plain

ブレークすると本当に Windows が丸ごとフリーズするので面白いです(他に活用できてない……)。ドライバー関連だと、 !devnode 0 1 とやると、デバイスマネージャーみたいな表示が得られます。

まとめ

ひとまず、ログを眺めながらデバッグすることができるようになって一安心です。しかし、 KMDF の内部ログとかを含めて見たい場合は、また別なことをしないといけないっぽく面倒だなぁといったお気持ちです。

MSBuildのバッチ機能とは

みなさん MSBuild プロジェクトの手書き、していますか?プロジェクトの手書きはさまざま苦労はあると思いますが、そんなご時世に、知っているとちょっと楽に書けて、しかもインクリメンタルビルドがしやすくなるバッチ機能について、詳しく見ていきましょう。

バッチの概要と使い方については、公式ドキュメントに書いてありますが、腑に落ちないというか、あまり理解できた感じがしなかったので、 MSBuildソースコードとステップ実行してみた結果を基に、具体的な動作から挙動を説明していきます。

  • 用語について
  • 基本的なバッチの動作
  • タスク
    • 簡単な例
    • 同じ値を持つメタデータ
    • 複数の項目の種類を使う
    • 項目名を指定しないでメタデータを参照する
    • バッチが適用されるパラメータ
  • ItemGroup
  • PropertyGroup
  • ターゲット
    • ターゲットのバッチ実行の基本
    • 悪用
    • プロパティや項目を参照するときの注意
  • バッチ実行計画を作成するプログラム
  • 終わりに
続きを読む

はじめての仮想HID

半年ぶりです。崇高な計画を遂行するために、仮想 HID が欲しくなったので、ドライバーから作ってみようと思い、いじってみた記録を書いておきます。

HID ドライバーのサンプル

Microsoft 公式のドキュメントで HID ドライバーのサンプルが公開されています。

これをパクれば簡単ですね!

……ミニマルサンプルにしてはでかすぎて理解できない。

というわけで、これをすべて理解する前に、もっと簡単そうな方法を見つけたので、それを試してみました。

Virtual HID Framework

Windows 10 から Virtual HID Framework(略して VHF)が登場しました。今までの HID ドライバーでは、 Windows が用意してくれるのは「これは HID ですよ」と宣言してくれる機能くらいで、多数の I/O リクエストがパススルーされてくるので、それに対応する処理をすべて書く必要がありました。対して VHF を使用したドライバーは、それ自身が HID として振舞うのではなく、 HID の作成、削除、 I/O 処理を管理するドライバーとして振舞います。このことから、ドキュメントでは「HID source driver」と呼ばれています。

VHF を使った HID ソースドライバーの作り方の公式ドキュメントはここにあります。

これを読めば満足という方は、このブログを閉じても大丈夫です。

ドキュメントの「Virtual HID device tree」の図を見てもらうと、 HID ソースの働きがわかると思いますが、 HID ソースは、自身が 0 個以上の子 HID を持つデバイスとして振舞います。 Windows のドライバー業界でバスと呼ばれているやつです。子 HID には Windows に組み込まれている VHF 用の仮想 HID ドライバーが使用されます。

バイスマネージャーの「接続別」で表示するとこんな感じになっています。

f:id:azyobuzin:20180725025411p:plain

HID ソース → 仮想 HID と接続され、 HID の種類からキーボードであることを認識してキーボードのドライバが読み込まれています。

続きを読む

MPMアルゴリズムのNSDFの求めかた

結論だけ知りたい人向け

NSDF の式

\displaystyle n'_t(\tau) = \frac{2r'_t(\tau)}{m'_t(\tau)} \quad (0 \leq \tau < W)

m'_t(\tau) について、前から求めると

\displaystyle
m'_t(\tau) = \begin{cases}
    2r'_t(0) & (\tau=0) \\
    m'_t(\tau-1) - x_{t+\tau-1}^2 - x_{t+W-\tau}^2 & (0 < \tau < W)
\end{cases}

後ろから求めると

\displaystyle
m'_t(\tau) = \begin{cases}
    0 & (\tau=W) \\
    m'_t(\tau+1) + x_{t+\tau}^2 + x_{t+W-\tau-1}^2 & (0 \leq \tau < W)
\end{cases}

となるので、好きな方でやってください。

はじめに

ピッチ検出アルゴリズムについて検索すると、まぁ大体このページ(音声の波形からピッチを検出するアルゴリズム - まめめも)に行き着くと思うのですが、 NSDF のインクリメンタルな求め方について特に説明もなく、クワイン用の well compressed なプログラムしか書いていないので、基の論文であるところの『A Smarter Way to Find Pitch』に書いてある式を変形しながら、本当にインクリメンタルに求まるのか検証していきます。

ここでは Normalized Square Difference Function の略として NSDF と言っていますが、 McLeod 氏の 2008 年の論文では specially-normalized autocorrelation function で略して SNAC と呼んでいますね。まぁいいや。

続きを読む

さぁ fixed を捨てて Unsafe だ

f:id:azyobuzin:20170929224302p:plain

早すぎる最適化が好きな人のための C# 7 の有効活用ガイドです。

ある構造体をそのまま byte 配列に突っ込みたくなるとき、ありますよね?構造体ならメンバーに名前がついていて書きやすい、でも相手が byte 配列だから 1 バイトずつ手書きするしかないのか……?そんなときにおすすめの技を紹介します。

達成目標

例を用意しましょう。 X Window Systemプロトコルは C 言語などでクライアントを実装しやすいように、適度にアライメントされたデータをやりとりします。しかもエンディアンもクライアント側が指定することができるので、クライアントはまさに構造体を直接送受信することができます。そこで、クライアントが X サーバに接続して、最初に送信するメッセージを構造体を使って中身を用意し、 byte 配列に書き込むことを目標にしていきましょう。

(この目標設定は、ちょうど X クライアントを C# で書いていたからなのですが、完成したころに公開します。公開しないかもしれません。)
公開してあります → X11Client.cs

続きを読む

それでも僕はWineでMonoを動かしたかった

Wine 上で動く Windows アプリを制御するにはどうしよう?と考えた結果、制御する側も Wine 上で動かせば Windows 上と同じように操作できるじゃないか!と思いやっていった記録。残念ながら、結局 Docker 上で動かすことはできなかったので、僕の意志を引き継いでくれる方、もしくは先行研究がありましたら教えてください。

というわけで、 Docker で動かすのを断念するまでに得た知見をまとめておきたいと思います。

環境

  • Ubuntu 17.04
  • Wine 2.14 win32 prefix
  • Mono 5.2.0 x86

wine-mono?それともスタンドアロンの Mono?

wine-mono は Mono のコアライブラリ + .NET Framework になりすますための mscoree.dll という構成になっています。つまり .NET の exe ファイルを起動しようとすると Mono を使ってくれる、という構成になります。

それに対して、 mono-project.com からダウンロードできる Mono の Windows 版は mono.exe を使ってアセンブリを実行する形をとります。つまり Linux で使っている Mono とまったく同じです。

これらふたつは共存することができるので、間接的に .NET Framework を必要とするアプリには wine-mono でうまくやっておいてもらいつつ、 .NET アセンブリだと最初からわかっているものは mono.exe で起動するという使い方ができるでしょう。

さて、最初は wine-mono で全部なんとかしようと思っていたのですが、厄介な問題にぶち当たりました。 wine-mono はアセンブリの厳密名を検証するようですが、回避するために App.config に <bindingRedirect> を書いても読み込んでくれないようです。というわけで、基本的には mono.exe を使うべきだということがわかりました。

続きを読む

I/O待ちのためのTaskとバックグラウンド処理のためのTask

ポエポエ~~

の一連のツイートのまとめでもしておこうかと。

I/O 待ちのための Task

I/O 処理を OS に投げて、完了を待機するのを抽象化した Task を「I/O 待ちのための Task」と呼んでいきます。身近かつ純粋な I/O 待ちを行う Task の例として、 FileStream を FileOptions.Asynchronous オプションをつけて開いたときの ReadAsync が挙げられます。

例として、 ReadAsync を await したときの挙動を見てみましょう。

async Task FileReadSample()
{
    const int bufSize = 10;

    // FileOptions を指定するために省略できないオプション大杉
    using (var fs = new FileStream(
        "test.txt",
        FileMode.Open,
        FileAccess.Read,
        FileShare.ReadWrite,
        bufSize,
        FileOptions.Asynchronous))
    {
        var buf = new byte[bufSize];

        var readBytes = await fs.ReadAsync(buf, 0, bufSize);

        Console.WriteLine(readBytes);
    }
}

「FileReadSample 続き」がどこで実行されるかは、このメソッドがどんな SynchronizationContext で呼び出されたかによりますが、とにかく OS からの非同期処理完了をスレッドプールが管理している I/O 完了ポートで待ち受け、 Task の完了を通知して、継続タスクを実行するという動きをします。

このとき ReadAsync 単体で見ると、 I/O 完了ポート用のスレッドが足りていれば、新しいスレッドを作成することも、スレッドプールのキューに新たにタスクを追加することもありません。つまり「I/O 待ちのための Task」は OS の非同期処理 API をラップし、継続タスクへの通知を行うだけのものといえます。

ただし、 await は SynchronizationContext が特別な場合(UIスレッドなど)を除き、続きをスレッドプールのキューに追加します。これはコールスタックが大きくなるのを防ぐためらしいですが、それが嫌なら ContinueWith で TaskContinuationOptions.ExecuteSynchronously を指定することで、完了通知を受け取ったスレッド上でそのまま続きが実行されます(本当?)。

より詳しく: Async訪ねて3000里

スレッドプールの有効利用

I/O 待ちのための Task は、スレッドプールを有効的に利用するために使います。

スレッドプールは短時間で終わる処理が大量にあるときに効率良くスレッドを利用する手段ですから、 I/O 待ちでスレッドプールのスレッドをブロックしてしまうのはスレッドプールの効率を落としてしまいます。そこで I/O を非同期で行い、完了を待っている間にキューに溜まった他の処理を実行できるように、I/O 待ちのための Task を利用します。

逆に、スレッドプール内で動いている Task でバックグラウンド処理のための Task を await するのは得策ではありません。なぜなら、そのまま続行できる処理をわざわざスレッドプールのキューに入れることになり、切り替えコストが発生するからです。

スレッドプールを有効利用することが求められるのは、web アプリケーションとライブラリです。

まず、 web アプリケーションでは、リクエストごとの処理がスレッドプールのキューに追加されます。ここでスレッドをブロックするようなコードを書いてしまうと、十分な性能を出すために多くのスレッドが必要となってしまいます。スレッドプールに割り当てるスレッドが多くなりすぎると、メモリ使用量がどんどん増えていってしまいますし、スレッドの管理コストも上がってしまいます。このような理由から、I/O 待ちのための Task を利用すると必要なスレッド数を減らせて、リソースを削減できます。

次にライブラリですが、ライブラリはあらゆる .NET アプリケーションから使われるので、 web アプリケーションから使われることを考えると同じことが言えます。

バックグラウンド処理のための Task

UI スレッドのフリーズを防ぐため、または並列処理のために処理を分割したものを「バックグラウンド処理のための Task」と呼んでいきます。バックグラウンド処理のための Task は、 CPU で大量の計算を行うときに Task.Run などを使って作成します。

「~Async」メソッドの罠

「~Async」という名前がついているメソッドは、一般的なライブラリの場合 I/O 待ちのための Task を表していることが多いです。理由は「スレッドプールの有効活用」で示した通りです。

I/O 待ちのための Task を返すメソッドが実際に非同期で処理するのは I/O だけです。だから、 I/O が発生する前に重い計算をしていても、それはメソッドを呼び出したスレッドで実行されているかもしれません。もし、 GUI アプリケーションでそのようなメソッドを UI をフリーズさせずに実行したいならば、

var taskResult = await Task.Run(async () =>
{
    var fooResult = await FooAsync();
    // 後処理
});
// taskResult を UI に反映したり

といったように、バックグラウンド処理のための Task に変換する必要があります。

また、 Task を返すメソッドだからといって、必ず非同期処理が行われるわけではありません。例えば、条件によって I/O が発生しないメソッドは、条件を満たすときに完了済みタスクを返します。 await は Awaiter の IsCompleted が true のとき、すなわち完了済みタスクが返ってきたときには、現在のスレッドのまま処理を続行します。このことを覚えておかないと、「メソッド内の1回目の await で ConfigureAwait(false) を指定したから、2回目からは要らないよね」とか言って死にます。

バックグラウンド処理のための Task の乱用防止

スレッドを切り替えずに済む処理をわざわざ Task にする必要はありません。処理を別スレッドに移譲する必要があるのは GUI アプリケーションくらいでしょう。ライブラリ開発者はこのことを頭に入れて、 I/O に関する部分だけを TaskAsync メソッドとして公開するべきです。そして、 GUI アプリケーション開発者は必要に応じて自分で重い処理を Task で包むようにします。このようにすることで、スレッドプールを有効に利用したコードが書けるのではないかと思います。