2012/05/13

Fusion で Mac OS X のカーネルデバッグ

● Kernel space is the last frontier 

...とまでは言わないが、しかしカーネルコードをみたりいじったりするのは非常に楽しい。一方で、カーネル空間でのミスは簡単にOSクラッシュを引き起こす。なんだかんだいって OSがなんとかしてくれるユーザランドでのアプリケーション開発やブラウザの上のスクリプト書きとは違う厄介さがある。

通常、カーネル内に組み込まれるコードのデバッグには2マシンデバッグが行われる。開発中のコードを実行するテストマシンとは別にデバッグ用のマシンを用意し、何らかの方法で接続、デバッグ用のマシンでデバッガを動かし観察するというものだ。接続方法は、昔ならシリアルケーブル、最近だと(少々癖はあるが) TCP/IP が利用できる。
もっとも、私はこれまで2マシンデバッグできる贅沢な環境でコードを書いたことがない。昔作った W-ZERO3用のシリアルドライバは、気合い一発の printf デバッグだった...、ファイルに書き出す前に落ちるので、WindowServer 殺してコンソールモードにしてログをはき出させ、死んだ瞬間の画面をメモって開発するという。今にしてみれば呆れた方法だった。

Mac OS X はネットワーク越しの 2マシンデバッグをサポートしている。方法については Mac OS X のドキュメントに記載があるので読んでみると良い。

とはいえ、2台もMacを横に並べて開発を行うのは少々面倒だ。仮想環境を使えばこの2マシンデバッグも簡単にできるのではないか?という事で試してみた。


● 仮想環境でカーネルデバッグを行うには

まず Mac Developer Program には入っておいた方がいい。年 $99 の有償プログラムだが、その価値はあるだろう。

開発環境である Xcode は無償でダウンロードできる。なので有償プログラムに入らなくてもアプリケーションや KEXT の開発は出来なくもない。ただし Xcode 以外の開発リソース、例えばカーネルシンボルなどその他配布物が有償登録せずに手に入るかは確認していない。


Fusion を使った2マシンデバッグの大枠の手順は、以下のページに既に記載されている。先の Apple のドキュメントとあわせて読んでおくといいだろう。
"Reverse Engineering Mac OS X : Mac OS X Kernel debugging with VMware"
http://reverse.put.as/2009/03/05/mac-os-x-kernel-debugging-with-vmware/
上記の記事は、まだ Lion のリリースされていない 2009 年のもののため、OS X のインストール方法が少々あやしいことを書いていたりするが、さくっと忘れてほしい。Lion の場合は インストールアプリケーションの下から InstallESD.iso さえ取り出せれば、細工せずとも普通にインストールが可能だ。

上記記事を参考に、私のやった手順を以下に記す。

以降、Xcode4 で開発を行い、ビルドを実行するホスト環境を Develop Machine、実際にコードを実行しテストを行うゲスト環境を  Target Machine と書く。


● ゲスト環境( Target Machine ) での準備

まず、仮想環境上に Mac OS X Lion をインストールする。

アップデートなどが終わったところで一度シャットダウン、停止状態でスナップショットをとっておく。今後何度も何度もクラッシュさせることになるので、安全な状態を確保しておくのは必要なことだ。

ゲスト環境のネットワーク接続は NAT のままで構わない。ただ、IPアドレスはDHCPではなく固定IPに書き換えておいた方が楽かも知れない。Fusion の DHCPサービスは概ね同じIPアドレスを割り当ててくれるが、変わると少々ややこしいことになるので気になる場合は固定IPに変更をしておくといい。

Develop Machine から Target Machine へファイルをコピーする手段を整えておく。ファイル共有を使ってもいいし、scp をつかっても良い。私は  Target Machine のアカウントの .ssh/authorized_keys に Develop Machine での自分のアカウントのSSHの公開鍵を入れておき、パスワードなしで簡単コピーできるようにしておいた。

続けて、カーネルデバッグ特有の設定を行う。

Apple のドキュメントだと NVRAM に引数を設定するようになっているが、仮想環境では nvram ではなく /Library/Preferences/SystemConfiguration/com.apple.Boot.plist に記載する。このファイルは管理者権限ではないと編集できないので、sudo  を使って vi なり nano なりで編集して欲しい。

デフォルトでは、
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Kernel Flags</key>
<string></string>
</dict>
</plist>
となっているのを
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Kernel Flags</key>
<string>-v debug=0x04 pmuflag=1</string>
</dict>
</plist>
に修正する。「-v」は verbose オプションで、起動時に黒画面に起動メッセージが表示されるようになる。

-v を指定すると、起動中のメッセージが表示される
さらにカーネルクラッシュした場合に、画面の上にコンソールメッセージを出して止まってくれる。一般ユーザには忌み嫌われるクラッシュ画面だが、カーネル開発をしているときにはこの断末魔の悲鳴は重要なヒントになる。

通常のクラッシュ時の画面
debug= でカーネルデバッグを指定した場合
MACアドレスとIPアドレスが表示されているのが分かる
-v オプションを指定した場合

debug=0x04 はデバッグオプションの設定で、Apple のドキュメントによると以下の通りだ

シンボル
フラグ値
意味
DB_HALT0x01OS起動時に一時停止を行い、デバッガーの接続を待つ
DB_PRT0x02コンソール画面に kernel 内の printf 出力を表示させる
DB_NMI0x04NMI (Non Maskable Interrupt) 信号でOSを一時停止しデバッガーに落とす
DB_KPRT0x08kprintf の出力をシリアルポートに出す
DB_SLOG0x10外部のGDBではなく組み込みのddb(kdb)をデフォルトデバッガにする
 (ただし、通常のDarwinカーネルにddb は組み込まれていない)
DB_ARP0x20デバッガに ARP テーブルの操作やルータを介した通信を許す(セキュリティーホールになるので注意) -- なお、どのカーネルにも本機能は含まれていない
DB_KDP_BP_DIS0x08古いGDBをサポートする
DB_LOG_PI_SCRN0x08グラフィカルなクラッシュ画面を表示させない

先の Reverse Engineering Mac OS X のドキュメントでは debug=0x01 を指定している。

 Target Machine の起動時に一時停止されるので、その間に Develop Machine の GDB で接続するというものだ。その理由として「仮想環境でどうやって NMI を起こせばいいか分からなかった(because I couldn’t find a way to create NMI events inside the virtual machine.)」とある。

確かに通常は「Command-Option-Control-Shift-Escape」か「Command-Power」キー操作でNMIが発生すると Apple のドキュメントにはあるが、仮想マシン内ではこれはうまく動かないようだ。

そこで Reverse Engineering Mac OS X では起動時に一時停止する手段をとったが、これだと起動のたびに GDB の atacch が必要になり面倒だ。そこで、強制的にデバッガを起動する KEXT 「PseudoNMI」を用意した。

ソースコードは GitHub に置いたので好きにみてほしい。
https://github.com/shiro-t/PseudoNMI

使い方は以下の通りだ。

  1. この PseudoNMI.kext をダウンロード
  2. パーミッションなどを調整する(所有権は root:wheel で、読み取りと実行のみに)
  3. sudo kextload ./PseudoNMI.kext」でカーネルに組み込む。
  4. sudo sysctl -w debug.pseudo_nmi=1」を実行する

sysctl を実行すると、内部的にNMIが発行されたのと同じ処理が動く。debug=0x04 が設定定されていれば外部デバッガーの接続待ちになり、そうでなければクラッシュする。


接続は TCP/IP を通じて行われる。ただし、そのままでは上手くいかないので、あらかじめ ARPテーブルに設定が必要だ。

ARP テーブルとは IPアドレスと Ethernet でのハードウェアアドレス(MACアドレス)を紐付けする対応表で、通常はカーネルが自動的に保守をしている。
カーネルは知らないIPアドレスへの通信が行われる際に ARP というプロトコルを使ってそのIPアドレスに対応するMACアドレスを手に入れている。その都度手に入れていたのではネットワークに負荷がかかるので、カーネル内のメモリに一定期間保管している。このARPテーブルの内容は「arp -a」コマンドで確認できる。

ARPによる処理は自動的に行われるので通常は気にする必要がない。ただ、デバッガ起動時はどうも自動的に手に入れた値を全てクリアしてしまっているようで、そのままでは通信できなくなる。そこで、あらかじめ固定的な値を組み込んでおく必要があるのだ。

手順としては、まず、Develop Machine 側の IPアドレスとMACアドレスを手に入れる。これは仮想マシンがどの方式でネットワークに繋がっているかによる。NAT の場合は vmnet8 というホスト側の仮想NICを通じて仮想マシンと繋がっている。このNATネットワークは 「192.168.x.0/24」のネットワークで、x の部分はインストール時に決定される。

そして、192.168.x.1 はデフォルトルータとして使われ、Develop Machine そのものの通信は 192.168.x.2 で行われる。
192.168.x.2 に対応する MAC アドレスは、Develop Machine  ifconfig vmnet8 を実行するか、 Target Machine 側で arp -a で ARP テーブルを表示させて確認できる。

IPアドレスと MAC アドレスの対が分かったら、 Target Machine 側で以下コマンドを実行する
sudo arp -s 192.168.x.2 aa:aa:aa:aa:aa:aa
これでOSが再起動されるまで削除されない ARPテーブルの設定がなされる。
仮想マシンのネットワーク構成を変えない限りこの値は一定のため、以下設定を /Library/LaunchDaemons におくことで起動の都度 自動設定させることも可能だ。


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>Label</key>
        <string>jp.nest.debug.static_arp</string>
        <key>ProgramArguments</key>
        <array>
                <string>/usr/sbin/arp</string>
                <string>-s</string>
                <string>192.168.203.2</string>
                <string>0:50:56:f6:cc:a8</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
</dict>
</plist>

このファイルを /Library/LaunchDaemons に配置したあとでリブートしてもよし、「sudo launchctl load /Library/LaunchDaemons/jp.nest.debug.static_arp.plist」を実行して読み込ませても良い。

これで、ゲスト側の準備は完了だ。


● ホスト環境(Develop Machine) でのデバッグ設定

さて、ホスト側の設定にうつる。

まず、Kernel Debug Kit を手に入れる。Kernel Debug Kit はシンボル情報(バイナリ中のどこがどの名前の関数に対応するかなどデバッグに必要な情報)が一式整ったものだ。OS のバージョンごとに存在するため、Lion の場合 kernel_debug_kit_10.7.3_11d50.dmg を手に入れる。入手先は http://developer.apple.com/sdk だ。

ダウンロード後、ダブルクリックしてイメージをマウントしておく。

必要に応じてカーネルのソースコードも手に入れておいた方がいいだろう。

http://opensource.apple.com から適切なバージョンを選んで、xnu-<xnuのバージョン> を選択、ダウンロードする。Lion (10.7.3) の場合は「xnu-1699.24.23」がそれだ。tar.gz アーカイブで手に入るので適当なところに展開しておく。カーネルソースがあれば、デバッガでソースコードを参照できる。


次に、ゲストで行ったのと同じ ARP テーブルの設定をホスト側でも行う。ゲスト側のNIC(en0)のIPアドレスと MAC アドレスを割り出した後
sudo arp -s <IPアドレス> <MACアドレス>
を実行する。Develop Machine側 vmnet8 のIPアドレスは常に一定だが、ゲスト( Target Machine )側の en0 のIPアドレスは、デフォルトではDHCPのため変わることがありえるので注意だ。冒頭にも書いたように、変わるのが面倒な場合は固定IPアドレスにしてしまうといい。

ここで準備は終了だ。

● ターゲットとなる KEXT を準備する

カーネルデバッグを行う最大の理由は、KEXT のデバッグであろう。

Xcode で開発を行い、KEXTをビルドする。ビルドすると KEXT と同じところに .sym のついたそのKEXTのシンボルファイルが生成されるのでこれも取得しておく。

開発した KEXT を  Target Machine にコピーする。
コピー後には 「所有者とグループを root:wheel に」「グループとその他の書き込み権限を削除」を忘れないように。セキュリティ上の都合から権限が甘い KEXT は読み込まれないようになっている。

KEXT が正しくロードできるかについては、kextutil というコマンドで確認できる。
sudo kextutil Foo.kext
なお、私はこの kextutil でチェックしてる時にクラッシュしたことがある。
出来の悪い KEXT だとそういう可能性もあるので注意してほしい。


● カーネルデバッグを行う

Develop Machine 側で GDB を立ち上げる
gdb /Volumes/KernelDebugKit/mach_kernel
Kernel Debug Kit をマウントしていると、/Volumes 以下にマウントされているはずだ。
以下コマンドを入力、準備を行う
gdb$ target remote-kdp
以下のコマンドを入力だけ行い、リターンを押さずに置いておく

gdb$ attach <Target Machine のIPアドレス>

 Target Machine  側にうつって以下コマンドを実行、PseudoNMI が存在するのを確認する
sysctl debug.pseudo_nmi
正常ならば「debug.pseudo_nmi: 0」と返ってくるはずだ。
存在していれば以下を実行する
sudo sysctl -w debug.pseudo_nmi=1
すると、実行した瞬間にゲストOS上の Mac OS X の実行が停止される。
すかさず Develop Machine 側で入力だけしておいた attach コマンドを実行する。

ARP テーブルが固定化されており、ちゃんと通信が出来るなら即座に「Connected.」と表示されプロンプトが表示される。もしそうならない場合は、ARPテーブルの設定忘れなので改めて設定をおこなってほしい。

接続できたら「gdb> c」 と c(ontinue) コマンドを実行する。すると、止まっていたゲストOSの実行が再開される。

これでGDBが接続された状態になっている。あとはブレークポイントを設定するなり、クラッシュしたらスタックトレースを引っ張るなりいつものデバッグができる。