KGDBのプログラムの動作を知るためには、その処理の流れを調べる必要があります。そのため、KGDBをデバッグして動作確認したいところではありますが、さすがにデバッガをデバッグするデバッガはありませんので、KGDBのソースにログ(デバッグログ)を仕込んで、動作を確認することにしました。
デバッグログを仕込む
デバッグログは、適切な箇所に埋め込まれているとバグ検出の強力な手助けとなります。今回は、KGDBの仕組みを理解することがメインとなりますので、KGDBソースの中で、ソフトウェア割り込みやブレークポイント設定箇所、そして、GDBとの通信箇所にアタリを付けて、処理の全体的な流れを推測しながら、その推測が正しいことを見るためのログを埋めることにしました。具体的には、以下を選択しました。
ログを仕込むポイント | KGDBソースに追加したデバッグログ | 説明 |
---|---|---|
KGDBのソフトウェア割り込みの入口と出口 | kgdb_handle_exception pc=<PC> enter | KGDBのソフトウェア割り込み処理関数(kgdb_handle_exception())の入口に仕込んだログ。<PC>は、割り込み発生時のプログラムカウンタ |
kgdb_handle_exception exit | KGDBのソフトウェア割り込み処理関数(kgdb_handle_exception())の出口に仕込んだログ | |
ブレークポイントの設定と解除契機 | kgdb_activate_sw_breakpoints: addr=<アドレス> | ブレークポイントに未定義命令を埋め込む処理関数(kgdb_activate_sw_breakpoints())に仕込んだログ。<アドレス>は、ブレークポイント設定アドレス |
kgdb_deactivate_sw_breakpoints: addr=<アドレス> | ブレークポイントに元の命令を埋め込む処理関数(kgdb_deactivate_sw_breakpoints())に仕込んだログ。<アドレス>は、ブレークポイント設定アドレス | |
GDBとの通信(送信/受信)パケット情報 | get_packet packet=<パケット> | KGDBのパケット受信処理関数(get_packet())に仕込んだログ。<パケット>は、GDBからKGDBに対して送られてきたパケット(文字列) |
put_packet packet=<パケット> | KGDBのパケット送信処理関数(put_packet())に仕込んだログ。<パケット>は、KGDBからGDBに対して送るパケット(文字列) | |
表7 デバッグログ |
いざKGDBをデバッグ!
それでは、KGDBソースへのデバッグログの仕込み、ソースをコンパイルしたカーネルイメージを組み込みボードのROMに焼き込みして、再起動すれば準備完了です。この具体的な方法については、【後編】で解説しますので、今回は、KGDBをどうやってデバッグしたのかに注力していきたいと思います。
KGDBをデバッグするためには、当然、KGDBを動作させる必要があり、カーネルのデバッグ操作を行う必要があります。そのため、今回デバッグする対象の関数として、さまざまなアプリケーションが使用するread(2)システムコールを使い、この関数の先頭にブレークポイントを設定することにしました(注7)。
なお、この関数のアセンブラ命令は以下の通りで、実際のブレークポイントの設定箇所はメモリ上のアドレスであるc00a9d9c番地です(注8)。
それでは、これからGDBコマンドをたたきながら、仕込んだログの内容を確認していきましょう。
デバッグ操作として、まずはsys_read()関数にブレークポイントを設定し、コンティニューしてみます(図10)。この操作を実行したときのログを以下に示します。
図10の左側の列がKGDBのデバッグログで、右側の列がGDB端末のログです。このログは、ブレークポイントを設定して、コンティニュー命令を実行するまでのKGDBの仕組みを理解するためには非常に大切なログです。それでは、このログの内容を処理のシーケンスに書いて、1つずつその仕組みを確認していきましょう。
まず面白いのは、ブレークポイントを設定したとき(1)には、GDBからブレークポイントの設定要求がでない点です。実際に設定されるのは、ユーザーがコンティニュー要求をしたタイミング(2)です。これは、ユーザーのコマンド操作では複数のブレークポイントを設定したり、途中でキャンセルしたりする可能性があり、そのような操作で無駄な通信が発生するのを避けているのだと思います。
ユーザーがコンティニュー操作をすると、GDBは要求されたブレークポイントの設定要求とコンティニュー要求を連続してKGDBに送信します(3)(4)。これを受けて、KGDBは、コンティニュー要求の契機で、ブレークポイント(c00a9d9c)に未定義命令を設定していることが分かります(5)。その後、KGDBはソフトウェア割り込み処理を終了し、CPUは元の処理を継続します(6)。
これで、ユーザーが指定したブレークポイントに未定義命令が埋め込まれましたので、いずれCPUがこのアドレス番地を踏むと、ソフトウェア割り込みが発生して、KGDBに処理が移ってくるということが読み取れました。
その後、CPUが処理を継続すると、read(2)システムコールの先頭(c00a9d9c番地)の未定義命令を実行し、期待通りブレークポイントでソフトウェア割り込みが発生しました(図12)。
最初に確認するポイントとしては、KGDBの割り込み処理の先頭で未定義命令を消し、元の命令コードに戻していることが分かります(2)。
その後、ブレークポイントに到達した旨をGDBに通知します(3)。それを受けたGDBは、詳細情報を取得するために、レジスタ値を取得します(4)(5)。そして、ブレークポイントに到達したメッセージをユーザーに出力し、ユーザーからのコマンド入力待ちになります(6)。
なお、GDBは、KGDBが実際に未定義命令を消したかどうかは知らないので、z0コマンドでブレークポイントの解除要求を行っています(7)。
さて、ここまでで、ブレークポイントがどのように実現されているか分かりました。あれ? でもちょっと待ってください。ブレークポイントの設定情報が消えてしまっているので、次のコンティニュー命令をしたときにはブレークポイントで止まらなくなりますよ。うーん、どうするのでしょうか……。論より証拠です。次のコンティニューのログを確認してみましょう。
コンティニューすると(1)、あれれ? GDBは、コンティニュー要求を受けると、ブレークポイントの次のアドレス(c00a9da0)にブレークポイント設定要求をしていますね(2)。その後、コンティニュー要求を送って、処理を継続させています(3)。KGDBは、その要求通り、次のアドレスに未定義の命令を埋め込んで(4)、処理を継続します(5)。
なぜ、こんなことをしているのでしょう? 不思議ですねぇ〜。とにかく、次のログを追い掛けてみましょう。
CPUは、次の命令コードを実行すると、当然ながら未定義命令実行により、ソフトウェア割り込みが発生します(1)。この際、KGDBはコードを元に戻し(2)、GDBに通知します(3)。それを受けたGDBは、自分が埋め込んだブレークポイントであることは承知の上のことですので、ユーザーには何も知らせず、そのブレークポイントの解除(6)に加えて、元のブレークポイントの設定要求(7)を行い、コンティニューしているようです(8)。そして、この要求によって、元のブレークポイントのアドレスに未定義命令が復活しました(9)!
なるほど、GDBが次の命令コードにブレークポイントをわざわざ設定していたのは、元のブレークポイントを復旧するのが目的だったのですね。
以上、まとめると、ブレークポイントでCPUを停止させるKGDBの仕組みは、以下の通りです。
普段、当たり前のように使っているデバッガの機能もその裏側ではいろいろと頑張ってくれているのですねぇ〜。
さて、今回は、自分が使うデバッガの仕組みを理解するために、KGDBをしっかりとデバッグしてみました。次回は、このKGDBを使うための方法を紹介します。ご期待ください!! (次回に続く)
Copyright © ITmedia, Inc. All Rights Reserved.