リモートデバッグの遊び方

目次

  1. はじめに
  2. リモートデバッグのしくみ
  3. gdbのbuild
  4. gdbserverもどきの実装
  5. gdbserverもどきの使い方
  6. その他
  7. 履歴

はじめに

シリアル接続されたPPCターゲットのリモートデバッグスタブ(gdbserverもどき) の使い方などをまとめてみました。ターゲットが本物PPCなのでイマイチ遊べる人が 少ないと思うのですが、そんときは単なる読み物として楽しんでください (<楽しめるのか?)。
gdbの日本語ドキュメントとして、 KI's Unofficial GNU Manual Translation Project を大いに参考にさせていただきました(GDBのページからもリンクされていて、 ほとんどofficialのような感じです)。こちらの方も合わせて参照して いたくと、より何かが(???)深まるかも知れません。

リモートデバッグのしくみ

リモートデバッグ時のハードウェア接続関係は以下のような感じになります。

接続図

シリアルケーブルは論理的な図になっています。実際は線2本ではありません(^^;
ホストPCでgdb本体が動作し、デバッガコマンドはシリアルポートを 通してターゲットボードに送られます。ターゲットボードではデバッグ を行うプログラムが動作するのですが、このプログラムにはホストgdb から送られてくるコマンドを解釈して応答する為のサブルーチンを 予めリンクしておきます(このサブルーチン群をデバッグスタブと呼びます)。 例えばgdbコマンドでメモリの内容を表示する場合、次のような手順で読み 出されます。

  1. gdbプロンプトで「x 0x300000」などと実行します。
  2. 「m300000,4」 というようなプロトコル(文字列そのもの)でターゲット ボードにメモリリードの要求が伝わります(図の赤い線の方向)。 ホスト側のgdbはその要求の応答待ちに入ります。
  3. ターゲットボード上でデバッグスタブはm300000,4を受け取り、アドレス 0x300000から4byte分のデータを実際に読み込みます。仮に内容は 0xAA55AA55とします。
  4. ターゲットボード上のデバッグスタブは、読み出したデータを 「AA55AA55という文字列」に変換してシリアルポートを介して ホスト側に返します(図の青い線の方向)。
  5. ホスト側gdbはターゲットスタブからの応答を受け取って、その 内容を表示します。

プロトコルには様々なものが用意されていますが、最低限スタブで解釈する べきプロトコルは、レジスタ内容のRead/Write、メモリ内容のRead/Write、 実行の再開、一命令分のステップ実行 といったものだけです。
もうひとつの方法として、gdbserverを使用する方法があります。こちらは デバッグを行うプログラムとホストgdbとの間で、例外ハンドルとホストgdb との通信の二つを行う専用のプログラム(server)を実行させる方法です。 serverさえ動作させれば、デバッグを行うプログラムには毎回スタブを リンクする必要がなくなるというメリットがあります。問題はターゲット ボード上で比較的まともに動作するOSが必要なことかも知れません。

今回は、後者の方式で遊んでみました。

gdbのbuild

適当なサイトからgdbのソースをダウンロードします。私はCygwin上でbuild しましたが、UNIX系OSなら特に問題なくbuildできると思います。 以下のような感じでbuildします。

./configure --target=powerpc-unknown-elf
make
これで PowerPCをターゲットとした gdb/gdb が生成されます。
gdb-5.0のアーカイブではシミュレータコンポーネントのmakeでエラー してずっこけるようですが、適当に直して多分OKです(^^;

gdbserverもどきの実装

サーバは大きく分けて以下のようなパートで構成してみました。

  1. ハードウェア例外のハンドリング部
  2. 受信プロトコルの解釈と送信プロトコルの生成部
  3. シリアルポート制御
ターゲットボード上でOSが動作している場合は、ハードウェア例外の 直接の例外ハンドリングはOSが受け持ち、スタブはOSが発行するシグナルを ハンドリングするような形になると思います。興味のある方は gdb/gdbserver/の下のソースを参照してみてください。

ハードウェア例外ハンドリング部の実装

PPCではいくつかのハードウェア例外が用意されています。例外が発生すると 例外に対応して決まったアドレス(例外ベクタ)にジャンプします。例外ベクタでは 全レジスタの内容をメモリに保存して、例外ハンドラにジャンプします。この レジスタを退避したメモリの内容がgdbでは、例外の起こった直後のレジスタの 内容として参照され、実行を再開するときに再びレジスタに復元されます。 set $r3=0xff00ff00 のようなコマンドでレジスタ内容を更新するときは、 レジスタ退避領域を書きかえるようなしかけになります。
例外ベクタにジャンプしただけではベクタアドレスを知らない限り、どのような 例外が起こったかをプロセッサ上で判断することができません。そこで、
vec0d00:
        /** Trace Exception **/
        stmw    %r0,0x80(%r0)
        xor     %r3,%r3,%r3
        oris    %r3,%r3,0x0d00       /** ←ここ **/
        b       __save_regs
のように、(ベクタアドレス / 0x100) の値を%r3の上位1byteに残しておくことで 例外ハンドラ内に、どのような例外でブランチしてきたかが判るようにして みました。これはプロトコル解釈パート(void handle_exception (int exceptionVector)) の引き数に与えて、handle_exception内でsys/signal.hで定義されるような シグナルに置きかえる処理を行っています。

受信プロトコルの解釈と送信プロトコルの生成部の実装

基本的にデバッグスタブそのものです(^^; 今回はsh-stub.cを参考に 変更してみました。例外ハンドラ内でエラーが起こった場合の処理など をはしょっているので、これで良いとは言いがたいものがあります(^^;

シリアルポート制御の実装

この部分はボード依存なのでソースには含んでいませんが、
void init_serial()         /*シリアルポートの初期化*/
char getDebugChar(void)    /*1文字受信             */
void putDebugChar(char c)  /*1文字送信             */
の三つのサブルーチンを作成すればOKです。

その他 ステップ実行について

ステップ実行の実装にはトレース例外を利用して実現してみました。この例外 はMSR[SE](マシンステータスレジスタのシングルステップトレースビット)を '1'にすることで発生し、1命令が正しく実行されるたびにトレース例外が 発生します(もちろん例外ベクタにジャンプしたときには'0'に落ちます。さも ないと例外ベクタ内で例外が発生して無限ループに陥ってしまいますから(^^;)。
いきなりセットしまうとその直後から例外が発生するため、例外からの復帰 時にセットします。具体的には例外処理から復帰するための専用命令rfiを 使用して、MSRをセットします。
        xor     %r8,%r8,%r8
        oris    %r8,%r8,stepping@ha
        ori     %r8,%r8,stepping@l
        lwz     %r9,0(%r8)
        cmpwi   %r9,0
        beq     __not_step
        mfsrr1  %r0
        ori     %r0,%r0,0x0400  /** set SE **/
        mtsrr1  %r0
        b       __esc1
__not_step:
        mfsrr1  %r0
        andi.   %r0,%r0,0xfbff  /** reset SE **/
        mtsrr1  %r0
__esc1:
        /*-----レジスタの復帰-----*/
        rfi        

gdbserverもどきの使い方

ターゲットボードへのロードと実行はそれぞれのボードで提供される方法 で行います。Windowsでターミナルソフトを使用して実行する場合、 即プロトコル受付け状態になるので、そのまま接続を切ります。
続いてgdbを起動します。bpsを指定する場合は--baudオプションを使用します。

gdb --baud 38400

続いてターゲットを切り替えます。以下の例ではCOM2ポートを使用しています。
(gdb) target remote com2
Remote debugging using com2
0x201c80 in ?? ()
これで停止状態になっています(^^;
デバッグ実行を行うプログラムをロードします。
(gdb) load test
Loading section .text, size 0x2f64 lma 0x10000
Loading section .rodata, size 0x8 lma 0x12f64
Loading section .data, size 0x7d0 lma 0x22f70
Loading section .sdata, size 0x18 lma 0x23740
Start address 0x10000 , load size 14164
Transfer rate: 14164 bits/sec, 372 bytes/write
(14kbpsしか出ていないのがいまいちですが(^^;それはさておき)
デバッグシンボルをロードします。これには予めコンパイル時に -g オプションを 付けてコンパイルしておく必要があります。
(gdb) file test
A program is being debugged already.  Kill it? (y or n) y

Reading symbols from test...done.
これで普通にリストコマンドなどを使用できます。
謎なのですが、ここでもう一度ターゲットを切り替えます。
(gdb) target remote com2
Remote debugging using com2
0x10000 in _start ()
ここでレジスタの内容を確認します。
(gdb) info registers r1 pc
r1             0x2203f8 2229240
pc             0x10000  65536
r1はスタックポインタとして使用されるのですが、この内容はgdbserver内の スタック領域を指しているため、適当に変えた方が良いと思われます。 本来ここらへんはプログラムロード時にOSが設定する部分です。

実行は'c'コマンドで実行再開という形で開始します。gdbの一般的なコマンドは 普通に使用できると思います。

その他

基本的な動作を行う事に特化しているため、

  1. 命令キャッシュの同期が考慮されていない(従ってキャッシュOFFでしか動作しない)。
  2. アドレス変換は使用していない。
  3. スーパーバイザモードで突っ走り。
  4. プロセッサ固有レジスタの表示を行っていない。
  5. 浮動小数点命令は使用できることが前提。
  6. プロセッサ依存の例外が起こると死亡。
など、凝ったことをするには制限が山積みですが、その辺は適当に改良して 遊んでいただくのが良いかと思います。

履歴

    2001.05.06 : 初版


TOP PREV