(H8移植編その2第17回)GDBのステップ実行対応

2012/04/03

あなたは 人目のお客様です.

サイボウズラボの開発合宿でGDB対応をしてからまただいぶ間が開いてしまった. そして実は今年初めての更新だったりするのだが,実は合間を見てステップ実行対応 の方法をいろいろ調べてネタをまとめていたのだ.

というのはどうもH8はステップ実行割り込みが無く,ステップ実行対応がすんなりとは 実装できない.ちなみにステップ実行割り込みがある場合には,CPUのモードレジスタに ステップ実行ビットみたいのがあったりするのでそれを立てて実行再開するだけで いい.1命令してまた割り込みが入ることになる.注意として,モードレジスタの 設定を変更する場合にはレジスタの値をいきなり書き換えるのでなく,スタブ内で registers[]とかに保存されている値を書き換えること.このようにしておけば, 処理再開するときに勝手にステップ実行ビットが立ってくれるわけだ.

まあでもH8ではステップ実行割り込みが無いので,なんとかソフトウェア的に 実装する必要がある.実はGDBに付属しているM32R向けのスタブの実装を見ると そんなようなコードがあって (gdb のソースに付属している m32r-stub.c の prepare_to_step() という関数だ) ,どうも次の命令をスタブ側でトラップ命令に置き換えて,トラップ割り込み みたいなのを発生させることで1命令単位で実行させているようだ. なのでH8でもそんなような実装をすればいいだろう.

実装時の注意として,通常はPC(ここでいう「PC」は「パソコン」ではなく 「プログラム・カウンタ」のことね)が指している命令の次の命令をトラップ命令にして おけばいい(1命令実行した直後にトラップが発生するようにしておく). もちろん,トラップ割り込みが発生した後にはもとの命令に書き戻してからスタブの 処理を行うことになる(つまり,GDB側はそのように書き換えられて実行されている ことは知らないことになる).

問題は条件分岐があった場合だ.この場合には分岐先と分岐しなかった場合の 2箇所にトラップ命令を仕込む必要がある.ということでスタブ側にはPCの指す先の 命令コードを読み,次の命令コードをトラップ命令に書き換え,さらに分岐命令や ジャンプ命令ならばその飛び先も書き換えるような処理を実装する必要がある.

さらにH8は可変長命令なので,もうひとつの問題がある.次の命令の場所はPCが 現在指している命令の命令コードを解析し,命令長を知ってそのぶん進めた先が 次の命令になる.なので命令長の解析処理が必要だ.

さらに実際にトラップ命令でブレークした場合に,PCはその先の命令に進んでしまって いるので,PCを巻き戻す必要がある.というのは本来ステップ実行割り込みでブレーク したのならば,トラップ命令の位置にあった本来の命令を実行する前にブレークして いるはずで,それがトラップ命令を実行することによってブレークしているため, ひとつ先に進んでしまっているからだ.でないと実行再開時に,本来そこにあった 命令が実行されずにとばされてしまうことになる.

ということで以下の4つの処理が実装できればいいことになる.

問題は分岐命令などの飛び先の取得と,命令長の取得だ.これはさすがにH8の マニュアルを読んで,命令コードのフォーマット解析が必要にある.

でも実は必要なマニュアルはH8/3069Fマイコンボードの添付CD-ROMに入っていたり するので,これを見ればよい.「h8_300h.pdf」「h8_3069f.pdf」という2つの PDFファイルだ. またマニュアルはルネサスエレクトロニクスのホームページからもダウンロード できるので,CPUのドキュメントのダウンロードの練習がてら,自分で探してみても いいだろう.

これらのマニュアルを見ると,命令コードのフォーマットや条件分岐時のアドレスの 指定方法などが載っている.アドレスの指定方法は「アドレッシングモード」という 言葉をキーワードに調べるといいだろう.マニュアルだけではわかりにくい部分も あると思うが,あとはobjdumpで逆アセンブルしてみたりとか,バイナリエディタで 機械語コードを適当に書き換えてやはり逆アセンブルしてみたりとかしてちまちま 調べるとまあだいたいわかってくる. 命令長に関しても,マニュアルをよく読んで適当に実装してやる.

また以下も参考になる.

で,これらを実装してステップ実行できるようにしたのが以下.

前回に対しての今回の修正点は,osのstub.cのみだ.

まずはmemcpy()を使いたいので,スタブ専用の互換関数を実装する. スタブ専用の関数を使う理由は前回を参照.

diff -ruN -U 10 h8_16/os/stub.c h8_17/os/stub.c
--- h8_16/os/stub.c	2011-12-29 17:58:39.000000000 +0900
+++ h8_17/os/stub.c	2012-04-03 18:40:20.474467000 +0900
@@ -56,20 +56,30 @@
 static char *stub_strcpy(char *dst, const char *src)
 {
   char *d = dst;
   for (;; dst++, src++) {
     *dst = *src;
     if (!*src) break;
   }
   return d;
 }
 
+static void *stub_memcpy(void *dst, const void *src, int size)
+{
+  char *d = dst;
+  const char *s = src;
+  for (; size; size--, d++, s++) {
+    *d = *s;
+  }
+  return dst;
+}
+
 static int a2h(unsigned char c)
 {
   if ((c >= '0') && (c <= '9'))
     return c - '0';
   if ((c >= 'a') && (c <= 'f'))
     return c - 'a' + 10;
   if ((c >= 'A') && (c <= 'F'))
     return c - 'A' + 10;
   return -1;
 }

さらにトラップ命令書き込み時の命令コードの保存用の領域の定義を追加.

@@ -217,20 +227,251 @@
     val = (val << 4) | v;
     p++;
     size++;
   }
   *valp = val;
   *pp = p;
 
   return size;
 }
 
+#define H8_300H
+#define BREAK_CODE "\x57\x30"
+#define BREAK_CODE_SIZE 2
+
+struct instruction {
+  void *addr;
+  char byte[BREAK_CODE_SIZE];
+} instructions[2];

命令コードを解析して命令長を取得する関数を実装.マニュアルを読んで マトリックスで値取得するような感じにしてみた.ほとんどの命令長は2バイト なのだが,10バイト命令があるって知ってた? (私は知りませんでした)

+static int get_instruction_size(void *addr)
+{
+  unsigned char *code = addr;
+  int bh, bl, dh, size;
+  static char size_all[0x100] = {
+    /*1 2 3 4 5 6 7  8 9 a b c d e f*/
+    2,1,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, /* 0 */
+    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, /* 1 */
+    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, /* 2 */
+    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, /* 3 */
+    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, /* 4 */
+    2,2,2,2,2,2,2,2, 4,2,4,2,4,2,4,2, /* 5 */
+    2,2,2,2,2,2,2,2, 2,2,1,1,2,2,4,4, /* 6 */
+    2,2,2,2,2,2,2,2, 8,4,6,4,4,4,4,4, /* 7 */
+
+    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, /* 8 */
+    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, /* 9 */
+    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, /* a */
+    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, /* b */
+    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, /* c */
+    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, /* d */
+    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, /* e */
+    2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, /* f */
+  };
+  static char size_6a[0x10] = {
+    4,0,6,0,4,0,0,0, 4,0,6,0,4,0,0,0,
+  };
+  static char size_6b[0x10] = {
+    4,0,6,0,0,0,0,0, 4,0,6,0,0,0,0,0,
+  };
+  static char size_01[0x10] = {
+    1,0,0,0,1,0,0,0, 2,0,0,0,4,4,0,4,
+  };
+  static char size_01_00_6b[0x10] = {
+    6,0,8,0,0,0,0,0, 6,0,8,0,0,0,0,0,
+  };
+
+  bh = (code[1] >> 4) & 0xf;
+  bl = code[1] & 0xf;
+  dh = (code[3] >> 4) & 0xf;
+
+  size = size_all[code[0]];
+
+  if (size == 1) {
+    switch (code[0]) {
+    case 0x6a: size = size_6a[bh]; break;
+    case 0x6b: size = size_6b[bh]; break;
+    case 0x01:
+      if (bl == 0x0) {
+	size = size_01[bh];
+	if (size == 1) {
+	  switch (code[2]) {
+	  case 0x69: size =  4; break;
+	  case 0x6d: size =  4; break;
+	  case 0x6f: size =  6; break;
+	  case 0x78: size = 10; break;
+	  case 0x6b:
+	    size = size_01_00_6b[dh];
+	    break;
+	  default:
+	    break;
+	  }
+	}
+      }
+      break;
+    default:
+      break;
+    }
+  }
+
+  return size;
+}

さらに分岐命令を調べて,分岐先を取得する関数を実装. アドレッシングモードがいくつかあるが,マニュアルや実際のバイナリコードを 見ててきとうに実装した.

+static void *get_jmp_addr(void *addr)
+{
+  unsigned char *code = addr;
+  unsigned long pc, jmp_addr = 0;
+  int regnum;
+  unsigned char *memaddr;
+
+  pc = (unsigned long)((char *)addr + get_instruction_size(addr));
+
+#define ULONG1(v0) ((unsigned long)((unsigned char)(v0)))
+#define ULONG2(v0,v1)    ((ULONG1(v0) <<  8) |  ULONG1(v1))
+#define ULONG3(v0,v1,v2) ((ULONG1(v0) << 16) | (ULONG1(v1) << 8) | ULONG1(v2))
+#define SLONG1(v0)    ((signed long)((signed char)(v0)))
+#define SLONG2(v0,v1) ((signed long)((signed short)ULONG2(v0,v1)))
+
+  /* 0x40〜0x4f (bXX) 8ビットPC相対分岐命令 */
+  if ((code[0] & 0xf0) == 0x40) {
+    jmp_addr = pc + SLONG1(code[1]); /* 符号拡張が必要 */
+  }
+
+#ifdef H8_300H
+  /* 0x5800〜0x58f0 (bXX d:16) 16ビットPC相対分岐命令(H8/300Hのみ) */
+  if ((code[0] == 0x58) && ((code[1] & 0x0f) == 0)) {
+    jmp_addr = pc + SLONG2(code[2], code[3]); /* 符号拡張が必要 */
+  }
+#endif
+
+  /* 0x59 (jmp @ERn), 0x5d (jsr @ERn) ジャンプ命令(絶対アドレス) */
+  if ((code[0] == 0x59) || (code[0] == 0x5d)) {
+    if ((code[1] & ~0x70) == 0) { /* 下位4ビットがゼロ以外のときは不正命令*/
+      /*レジスタ間接*/
+      regnum = (code[1] >> 4) & 0x7;
+      jmp_addr = registers[regnum];
+    }
+  }
+
+  /* 0x5a (jmp @aa:24), 0x5e (jsr @aa:24) 24ビットジャンプ(絶対アドレス) */
+  if ((code[0] == 0x5a) || (code[0] == 0x5e)) {
+    jmp_addr = ULONG3(code[1], code[2], code[3]);
+  }
+
+  /* 0x5b (jmp @@aa:8), 0x5f (jsr @@aa:8) 8ビットジャンプ(メモリ間接) */
+  if ((code[0] == 0x5b) || (code[0] == 0x5f)) { /* JMP/JSR @@aa:8 */
+    memaddr = (unsigned char *)ULONG1(code[1]); /* 8ビット絶対アドレス */
+#ifdef H8_300H /* 24ビット指定で最上位バイトは無視 */
+    jmp_addr = ULONG3(memaddr[1], memaddr[2], memaddr[3]);
+#else /* 16ビット指定 */
+    jmp_addr = ULONG2(memaddr[0], memaddr[1]);
+#endif
+  }
+
+  /* 0x55 (bsr d:8) 8ビットPC相対サブルーチンコール */
+  if (code[0] == 0x55) {
+    jmp_addr = pc + SLONG1(code[1]); /* 符号拡張が必要 */
+  }
+
+  /* 0x5c (bsr d:16) 16ビットPC相対サブルーチンコール */
+  /*
+   * 通常のブランチ命令では16ビットアドレス指定はH8/300Hでのオプションだが,
+   * bsrはH8/300でも16ビット命令が利用できるようだ.
+   */
+  if ((code[0] == 0x5c) && (code[1] == 0x00)) {
+    jmp_addr = pc + SLONG2(code[2], code[3]); /* 符号拡張が必要 */
+  }
+
+  /* 0x54 (rts,rts/l) 関数復帰 */
+  if (code[0] == 0x54) {
+    regnum = (code[1] >> 4) & 0x7;
+    memaddr = (unsigned char *)registers[regnum];
+#ifdef H8_300H
+    jmp_addr = ULONG3(memaddr[1], memaddr[2], memaddr[3]);
+#else
+    jmp_addr = ULONG2(memaddr[0], memaddr[1]);
+#endif
+  }
+
+  /* 0x56 (rte,rte/l) 割込み復帰 */
+  if (code[0] == 0x56) {
+    regnum = (code[1] >> 4) & 0x7;
+    memaddr = (unsigned char *)registers[regnum];
+#ifdef H8_300H
+    jmp_addr = ULONG3(memaddr[1], memaddr[2], memaddr[3]);
+#else
+    /* スタック先頭はCCRの値なので,PCは2バイト目以降から復旧する */
+    jmp_addr = ULONG2(memaddr[2], memaddr[3]);
+#endif
+  }
+
+  jmp_addr &= ~0xff000001;
+
+  return (void *)jmp_addr;
+}

さらに命令コードの保存と復旧処理,あとトラップ命令でブレークした場合の PCの巻き戻し処理を実装.

+static void store_instruction(struct instruction *inst, void *addr)
+{
+  inst->addr = addr;
+  stub_memcpy(inst->byte, inst->addr, BREAK_CODE_SIZE);
+}
+
+static void restore_instruction(struct instruction *inst)
+{
+  if (inst->addr) {
+    stub_memcpy(inst->addr, inst->byte, BREAK_CODE_SIZE);
+    inst->addr = NULL;
+  }
+}
+
+static int check_instruction(struct instruction *inst, void *addr)
+{
+  return (inst->addr && (inst->addr == addr)) ? 1 : 0;
+}
+
+static void init_softstep()
+{
+  instructions[0].addr = NULL;
+  instructions[1].addr = NULL;
+}
+
+static void set_softstep(unsigned long pc)
+{
+  void *addr = (void *)pc;
+  void *jmp_addr, *next_addr;
+
+  jmp_addr = get_jmp_addr(addr);
+  next_addr = (char *)addr + get_instruction_size(addr);
+
+  store_instruction(&instructions[0], next_addr);
+  stub_memcpy(next_addr, BREAK_CODE, BREAK_CODE_SIZE);
+
+  if (jmp_addr) {
+    store_instruction(&instructions[1], jmp_addr);
+    stub_memcpy(jmp_addr, BREAK_CODE, BREAK_CODE_SIZE);
+  }
+}
+
+static void reset_softstep()
+{
+  restore_instruction(&instructions[1]);
+  restore_instruction(&instructions[0]);
+}
+
+static unsigned long check_softstep(unsigned long pc)
+{
+  unsigned long prev = pc - BREAK_CODE_SIZE;
+  if (check_instruction(&instructions[0], (void *)prev)) return prev;
+  if (check_instruction(&instructions[1], (void *)prev)) return prev;
+  return pc;
+}

ステップ実行の場合には「s」というコマンドがgdbから送られてくるので, その場合には次命令をトラップ命令に書き換えるような処理を追加. ついでにメモリ書き換え命令(「M」)のときに「OK」を返していなかったバグを修正.

 static void handle_exception()
 {
   int sig = STUB_SIGTRAP;
   unsigned char *p;
   long addr, size;
 
   p = sendbuf;
   *(p++) = 'T';
   *(p++) = h2a((sig >> 4) & 0xf);
   *(p++) = h2a(sig & 0xf);
@@ -280,21 +521,25 @@
       if (*(p++) != ',') {
 	stub_strcpy(sendbuf, "E01");
 	break;
       }
       a2val(&p, &size);
       if (*(p++) != ':') {
 	stub_strcpy(sendbuf, "E02");
 	break;
       }
       write_memory(p, (void *)addr, size);
+      stub_strcpy(sendbuf, "OK");
       break;
+    case 's':
+      set_softstep(registers[REG_PC]);
+      /* fall through */
     case 'c':
 
       /*
        * breakpoint:
        remote.c:remote_insert_breakpoint()
        ->
        memory_insert_breakpoint()
        ->
        mem-break.c:default_memory_insert_breakpoint()
        ->

あとはスタブ処理の先頭でPCの巻き戻し処理とトラップ命令を元に戻す処理の 呼び出しを追加.

@@ -351,27 +596,30 @@
 {
   int send_enable;
   int recv_enable;
 
   send_enable = serial_intr_is_send_enable(SERIAL_DEFAULT_DEVICE);
   recv_enable = serial_intr_is_recv_enable(SERIAL_DEFAULT_DEVICE);
   serial_intr_send_disable(SERIAL_DEFAULT_DEVICE);
   serial_intr_recv_disable(SERIAL_DEFAULT_DEVICE);
 
   store_registers(sp);
+  registers[REG_PC] = check_softstep(registers[REG_PC]);
+  reset_softstep();
   handle_exception();
   restore_registers(sp);
 
   if (send_enable) serial_intr_send_enable(SERIAL_DEFAULT_DEVICE);
   if (recv_enable) serial_intr_recv_enable(SERIAL_DEFAULT_DEVICE);
 }

デバッガ動作開始時の初期化を追加.

 void set_debug_traps()
 {
   /* set interrupt vector for handling breakpoint. */
   softvec_setintr(SOFTVEC_TYPE_BKPOINT, break_intr);
+  init_softstep();
 }

修正したのはこんなところだ.実際に試すときには,前回説明したように ブレークポイント設定でsleep命令でなくトラップ命令を埋め込むように h8300_breakpoint_from_pc()にパッチ当てしてビルドしたgdbが必要. でないとブレークポイントでブレークできない.

あと試すときには Makefile を修正して -g と -O0 のフラグを立てておくこと. (-O0は必須ではないがデバッグする際には推奨)

実際に試すときは,以下のようにするといいだろう.

FreeBSDで試してみたらとりあえずステップ実行は動作した.ソースコードデバッグ できて面白い.ていうかこれってデバッグにちょう便利.


メールは kozos(アットマーク)kozos.jp まで