デバッグ方法

開発に参加していると、デバッグをせざるを得なくなると思うので、その方法を。

結構基本的な事が多い。

 

assertで落ちた場合

以下の画像のように、落ちたassertのファイル名と行番号が分かるので、比較的容易にデバッグ可能。

バックトレースも表示される。(後述)

screenshot-from-2016-11-08-21-48-57

 

panicで落ちた場合

カーネルパニックや、未定義割り込みが発生した場合は、以下のようなエラーが出る。

[クラス名] error: エラーメッセージ

エラーメッセージでgrepするだけだと辛いので、バックトレース(後述)を元に調べる事になる。

screenshot-from-2016-11-08-21-51-01

 

未定義割り込みで落ちた場合

割り込みが発生した時に実行していた命令のripレジスタ(プログラムカウンタ)とrbpレジスタ(スタックフレーム末尾)が表示されるので、バックトレース(後述)を調べる時と同様の手順を踏む。

screenshot-from-2016-11-08-21-55-21

ちなみに、この画像では二回GPが発生しているが、これは最初の割り込みに起因して実行されたエラー出力コード内で、バックトレースを表示しようとしてinvalidなメモリを踏んだ事によって新しいGPが発生したからである。

これは現在のバックトレース出力コードが、スタックの深さを考慮せずに定数回スタックフレームを辿る事に起因する。言うなればスタックアンダーフロー。本当は修正しなければいけない(アンダーフローもまずいし、最高3回しかスタックフレームを掘れないのも良くない)のだけど、手が回っていないので。。。

とりあえず今は無視しててください。あと、この現象は全てのバックトレース出力コードに共通しているため、panicやassert失敗でも生じうる。

アドレスからファイルと行番号を取得する

ここからようやく本題。

assertで落ちた時はファイルと行番号が取得できるけれど、多くの場合、どのアドレスの命令を実行中に落ちたか、という情報しか得られない。そのアドレスからファイルと行番号を取得するには、addr2line命令を使えば良い。

 

$ addr2line -Cife objname address

 

objnameはカーネルのelfバイナリ(プロジェクトのルートディレクトリで実行するなら、build/kernel.elf)、addressはエラーメッセージから取得した16進数16桁。もちろん、エラーを吐いたカーネルバイナリとaddr2lineに掛けるバイナリは同一でないとダメ。

 

バックトレースの調査

どこで落ちたか、が分かるようにはなったが、今度は落ちたコードがどこの関数から呼ばれていたか、が知りたくなる。

そこで、エラーメッセージと同時に表示されているバックトレース情報を使えば、関数の呼び出し元を追跡できる。

バックトレースについて簡単に説明すると、関数呼び出しのためにcall命令が実行されるとreturn先アドレスがスタック上に積まれる。一般的なC++コンパイラは、関数を実行する際に事前にrbpレジスタにスタックポインタを退避させておくので、関数内でエラーが発生した場合、rbpレジスタを元に、スタック上に積まれたreturn先アドレスを取り出す事ができる。また、一般的なC++コンパイラは、これまた関数を実行する際に古いrbpレジスタをスタックに退避させておくので、rbpレジスタを元に、関数呼び出し元が使用している、古いrbpレジスタを取り出す事ができる。このrbpレジスタを使えば更に「「呼び出し元関数」のreturn先」が分かる、という形で、スタックフレームをどんどん下に掘っていく事が可能になる。Raph Kernelがエラー時のバックトレースで表示しているのは、こうして算出されたreturn先アドレス(rip)である。

ゆえに、backtrace(0)で表示されているのが、エラーで落ちた関数の呼び出し、の次の式に相当するアドレス(return先を表示しているので)である。関数の呼び出し式を直接指せないのは構造上仕方ない事だが、これでもデバッグする上では十分だろう。

 

エラー情報が表示されずに落ちた場合

ここからがカーネル開発におけるデバッグの醍醐味である。エラーが出るなら、情報はあらかた出揃うし、何ならエラーが出る直前にデバッグ出力を挟んで、メモリダンプでもなんでもすれば宜しい。問題はエラーが出ない場合で、こうなってくると、これまでの経験と運と筋肉が大事になってくる。

 

ケースA: 何も画面に表示されない。どこかで落ちてる

画面出力が初期化される前に、panicで落ちてる。この場合は比較的簡単。

qemuのコンソールからinfo registersを叩き、 ripレジスタの値を取得する。

後はaddr2lineを使って、どこのコードで落ちたか調べれば良い。

rbpレジスタの値から手動バックトレースもできるようになっておくと尚良し。

qemuのコンソール上での(仮想)メモリダンプは、

(qemu) x /10x 0x(address)

時には落ちたアドレスの逆アセンブリをしたいかもしれない。そういう時は、

(qemu) x /10i 0x(address)

 

ケースB: 強制的にリセットが掛かる

こっちの方が調査がめんどい。しかし、どこかしらのコードが悪さをして落ちているのは確実なので、落ちるコードの直前にassert(false)を入れたり、強制panicで落とせば良い。

どうやって落ちるコードの直前を知るかって?カーネルの実行の流れに沿って、少しづつassert(false)の挿入位置をずらしていけば、いずれリセットが掛かるようになる。そこが問題のコードさ。簡単だね!