前回は、ICE無しでデバッグユニットを使う準備として、デバッグ用ベクタアドレスを0x0000000に切り替える実験を行いました。今回は実際に、デバッグユニットのデバッグ機能を利用してみます。
デバッグユニットは、以下の4種類のデバッグ機能を内蔵しています。
『S1C33000コアCPUマニュアル』「3.6.1 デバッグモードの機能」より
|
ソフトウェアブレーク機能は、既に前回のテストプログラムでも使いました。シングルステップ機能は、ちょっと使いづらいので後回しにしましょう。命令ブレークとデータブレーク機能は、どちらも有用なデバッグ機能です。今回は、命令ブレーク機能を実験してみることにします。
命令ブレーク機能を使ったテストプログラムを作りました。ソースコードは以下のとおりです。
|
テストプログラムを実行して、Aボタンを押すたびに「dbg_count = 0」の数値が増えれば成功です。終了するには、SELECT+STARTを長押ししてください。
テストプログラムの内容を見る前に、用語を確認しておきます。デバッグ関連のレジスタには、二種類のグループがあります。一つは前回見つけた「デバッグユニットの制御レジスタ」で、もう一つは今回使用する「デバッグ機能を制御するレジスタ」です。「デバッグユニットの制御レジスタ」は、略して「DBGの制御レジスタ」と表記されることもあります。「デバッグ機能を制御するレジスタ」は、略して「デバッグ用レジスタ」と表記されることもあります。
|
それぞれ、参照するマニュアルの記載箇所が異なることに気を付けてください。「デバッグユニットの制御レジスタ」は正式なマニュアルに記載されておらず、前回の記事に掲載したI/Oマップのコピーが資料の全てです。「デバッグ機能を制御するレジスタ」は正式なマニュアルに記載されているので、以下を読む際に各レジスタの詳細な説明はマニュアルを併せて参照してください。
それでは、テストプログラムの内容を見て行きます。プログラムを起動すると、pceAppInit()関数が呼ばれます。定型の画面表示設定を行った後、「デバッグユニットの制御レジスタ」のDBGVPとDBGVを使って、デバッグ用ベクタアドレスを0x0000000に切り替えます。前回述べたように、ICE無しでデバッグユニットを使うために必須の準備です。
|
次に、命令ブレークを設定します。命令ブレークを設定するには、「デバッグ機能を制御するレジスタ」のIBE0とIBAR0を書き換える必要があります。「デバッグ機能を制御するレジスタ」を書き換えるには、一時的にデバッグモードへ移行しなければなりません。そこで、brk命令を使ってソフトウェアブレークのデバッグ例外を発生させ、デバッグモードに切り替えてdbg_set_ib0()関数を呼び出します。dbg_set_ib0()関数は、test_func()関数の先頭アドレスを実行しようとしたときに命令ブレークのデバッグ例外が発生するよう、IBE0とIBAR0を設定します。その後、retd命令を使ってユーザーモードに切り戻し、呼び出し元に返ります。
|
|
pceAppInit()関数の最後では、この後、デバッグ例外が発生したときに、デバッグ処理ルーチンであるdbg_isr()関数が呼び出されるよう、デバッグ用ベクタを設定します。pceAppInit()関数の処理は、以上です。
|
pceAppProc()関数は、プログラム実行中に繰り返し呼び出される関数です。Aボタンが押されたら、test_func()関数を呼び出します。test_func()関数は何もしないダミー関数ですが、test_func()関数の先頭アドレスに命令ブレークが設定されています。test_func()関数を実行しようとすると、命令ブレークのデバッグ例外が発生してtest_func()関数の実行を中断し、デバッグモードに切り替わってdbg_isr()関数が呼び出されます。dbg_isr()関数は、グローバル変数dbg_countを1増やします。その前後では、グローバル変数dbg_countを1増やす処理に使用するレジスタの退避と復帰を行っています。デバッグ処理ルーチンにおけるレジスタ退避と復帰の手順は、一般的な割り込み処理ルーチンにおけるレジスタ退避と復帰の手順とは、やや異なります。その理由は、後ほど述べることにします。dbg_isr()関数の最後では、retd命令を使ってユーザーモードに切り戻し、中断していたtest_func()関数の実行を再開します。
|
pceAppProc()関数の最後では、デバッグ例外が発生してdbg_isr()関数が呼び出された回数を確認するために、グローバル変数dbg_countを画面表示します。pceAppProc()関数の処理は、以上です。
SELECT+STARTを長押ししてプログラムを終了しようとすると、pceAppExit()関数が呼び出されます。pceAppExit()関数では、pceAppInit()関数の中で設定した命令ブレークを解除します。pceAppExit()関数が命令ブレークを解除する手順は、pceAppInit()関数が命令ブレークを設定した手順と同様です。もし、命令ブレークを解除せずにプログラムを終了すると、P/ECEがハングアップしてしまいます。なぜなら、このプログラムを終了した後、test_func()関数やdbg_isr()関数があったアドレスには、別のプログラムがロードされているからです。test_func()関数があったアドレスの命令を実行しようとしてデバッグ例外が発生し、dbg_isr()関数があったアドレスが呼び出されます。しかし、もうそこにはdbg_isr()関数は無いので、不正な処理を実行してハングアップするのです。pceAppExit()関数の処理は、以上です。
|
|
テストプログラムを例にして、命令ブレーク機能の使い方を説明しました。あらかじめ設定しておいた条件に従ってプログラムの実行を中断し、処理ルーチンを呼び出す機構であるという点において、デバッグ例外と割り込みはよく似ています。説明の途中で、「デバッグ処理ルーチンにおけるレジスタ退避と復帰の手順は、一般的な割り込み処理ルーチンにおけるレジスタ退避と復帰の手順とは、やや異なります。」と述べました。その理由は、デバッグ例外と割り込みでは、CPUが自動的にレジスタを退避、復帰する内部動作が異なっているからです。以下に、なぜ異なっているのかを考察します。
割り込みが発生し、プログラムの実行を中断して割り込み処理ルーチンを呼び出すとき、CPUの内部動作は次のようになります。
|
割り込み処理ルーチンを終了するために、reti命令を実行したとき、CPUの内部動作は次のようになります。
|
デバッグ例外が発生し、プログラムの実行を中断してデバッグ処理ルーチンを呼び出すとき、CPUの内部動作は次のようになります。
|
デバッグ処理ルーチンを終了するために、retd命令を実行したとき、CPUの内部動作は次のようになります。
|
両者を比較すると、次のような違いがあります。
|
割り込みの挙動は、S1C33209以外のCPUの一般的な挙動と同じなので、直観的に理解できます。デバッグ例外の挙動が、少し特殊です。二つの疑問点が挙げられます。
マニュアルには理由が書かれていないので、推測してみることにします。
デバッグ機能は、その名のとおり、プログラムをデバッグするために利用する機能です。プログラムをデバッグするためには、デバッグ例外が発生してプログラムの実行を中断した瞬間の状態を監視できるよう、できるだけその瞬間の状態を変更せずにデバッグ処理ルーチンを呼び出す必要があります。もし、スタックにレジスタを退避する仕様だったとすると、デバッグ例外が発生した時点でプログラムが利用していたスタックトップ近辺のメモリ内容を破壊してしまうことになります。スタックトップ近辺のメモリ内容は、理論上は不定でありプログラムの実行に無関係なのですが、現実にはデバッグ時の参考になることが多いので、破壊されるのは望ましくありません。また、そもそもデバッグ例外が発生するような異常な状況ですから、スタックがメモリの無いアドレスを指している可能性も考えられます。もし、スタックがメモリの無いアドレスを指していて、%pcを格納できないと、デバッグ処理ルーチンからはデバッグ例外が発生したアドレスを判断できなくなってしまいます。以上のようなリスクを回避するために、スタックでなく、特定のアドレスにレジスタを退避、復帰する仕様になっているのだと思います。
以上の推測が正しければ、CPUが%r0を自動的に退避、復帰する理由も推測できます。デバッグ処理ルーチンが、特定のアドレスにレジスタを格納するためには、少なくとも一つ以上の汎用レジスタを破壊せざるを得ないからです。順を追って考えてみましょう。CPUが自動的に退避するレジスタ以外のレジスタで、デバッグ処理ルーチンの中で使用するレジスタは、デバッグ処理ルーチンの先頭で、明示的に退避しておく必要があります。先に述べた理由により、退避先はスタックでなく、特定のアドレスとすることが望ましいです。もし、CPUが%pc以外のレジスタを自動的に退避しない仕様だったとすると、デバッグ処理ルーチンの先頭では、どのレジスタも破壊できなくなってしまいます。どのレジスタも破壊せずに、特定のアドレスにレジスタを格納できるでしょうか。いくつか、試してみましょう。
|
これでは、特定のアドレスではなく、スタックに格納してしまっているので、ダメです。
|
一見、OKに見えますが、ダメです。「xld.w [address], %r0」という形式の命令は擬似命令で、アセンブラによって次のように展開されるからです。ご覧のように、%r9を破壊してしまっています。
|
P/ECEのプラットフォームに限定すれば、唯一、次のような方法があります。
|
ソースコードは先ほどと同じで、コンパイルオプション「-gp=0x0」を追加しただけの違いです。このソースコードは、アセンブラによって次のように展開されます。
|
P/ECEのプラットフォームでは、%r8が常にゼロ固定であることを、コンパイルオプションでアセンブラに指示できます。アセンブラは、コンパイルオプションを参照して、擬似命令をより効率的に展開するのです。この方法ならば、どのレジスタも破壊せずに、特定のアドレスにレジスタを格納できます。とはいえ、%r8が固定であるかどうかは、プラットフォーム設計者の任意なので、S1C33209を応用している全てのプラットフォームで、%r8が固定であるとは決め付けられません。また、P/ECEのプラットフォームでも、うっかり%r8をゼロ以外の値に変更してしまうと、P/ECEのアプリケーションやBIOSは、%r8がゼロである前提で作成されているため、正しく動作しなくなってしまいます。デバッグ処理ルーチンは、あらゆる異常な状況で呼び出される可能性があるので、%r8がゼロであると仮定するのは危険です。
というわけで、デバッグ処理ルーチンが特定のアドレスにレジスタを格納するためには、少なくとも一つ以上の汎用レジスタを破壊せざるを得ません。デバッグ例外が発生したときに、CPUが自動的に%r0を退避する理由は、デバッグ処理ルーチンが%r0を使って、特定のアドレスにレジスタを退避するためだと思います。CPUが自動的に全レジスタを退避してくれる仕様ならばもっと楽なのですが、もしそうだったとすると、デバッグ例外が発生してからデバッグ処理ルーチンが呼び出されるまでの時間が非常に長くなってしまいます。シビアなタイミングに依存したデバッグ時に、最小限のタイムラグでデバッグ処理ルーチンを起動できるよう、CPUは最低限のレジスタだけを自動的に退避する仕様としたのでしょう。
さて、スタックにレジスタを退避しなくても、デバッグ処理ルーチン自身がそのままスタックを使用していたのでは、何にもなりません。デバッグ処理ルーチン専用のスタックに切り替えるのが、正しい方法です。以上を踏まえると、全レジスタを退避し、かつ、専用のスタックに切り替える、厳密なデバッグ処理ルーチンの雛形コードは、次のようになります。
|
──厳密なのは良いのですけれど、やたら長いですね。デバッグ処理ルーチン専用のスタックも、メモリ負荷が高いです。本物のICE製品には厳密さが要求されますが、趣味のプログラムはここまで厳密でなくても良いかも知れません。もう少し、簡略化を検討してみましょう。
経験上、P/ECEのプログラミングにおいて、スタックがメモリの無いアドレスを指すようなバグは稀だと思います。また、スタックトップ近辺のメモリ内容を参照するようなデバッグもあきらめて、スタックにレジスタを退避してしまうことにしましょう。すると、デバッグ処理ルーチンの雛形コードは、次のように簡略化できます。デバッグ処理ルーチン専用のスタックも、不要になりました。
|
上のコードは汎用のため、全レジスタを退避していますが、デバッグ処理ルーチンの中で使用するレジスタがたとえば%r0と%psrだけならば、最低限のレジスタ退避だけで済ませても構いません。
|
今回の最初に提示した、テストプログラムのデバッグ処理ルーチンと同じコードになりました。
話題が脱線気味で、たいへん長くなってしまいました。以上、デバッグ処理ルーチンにおけるレジスタ退避と復帰の手順が、一般的な割り込み処理ルーチンにおけるレジスタ退避と復帰の手順と異なっている理由について、CPUの内部動作の考察もまじえて説明しました。デバッグ例外の挙動一つとっても、よく考えて設計されているのだなあ、と感心しました。ところで、他のCPUの場合はどうなのでしょうか。このクラスの組み込みCPU代表格の一つである、SuperH CPUのマニュアルを読んでみました。SuperHも、ユーザーブレークコントローラ(UBC)という名の、オンチップICE回路を内蔵しています。マニュアルによると、SuperHのデバッグ例外は、通常の割り込みと同じ挙動だそうです。つまり、単純にスタックにレジスタを退避します。それで充分という判断なのでしょう。デバッグ例外の設計方針は、CPUそれぞれみたいです。
次回は、データブレーク機能を実験してみようと思います。
(…続きます)