(H8移植編その2第16回)GDB対応をしてみた

2011/12/29

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

先日サイボウズラボの開発合宿に参加させていただきました. 関係者のかた,どうもありがとうございました.

雰囲気としてはもくもく会によく似た感じで各自が自分のテーマに沿ってもくもくと開発を 行うというもので,いろいろとお話もできたしとても楽しめた. で,ぼくはKOZOSのGDB対応というテーマで進めたのだが,期間中になんとかそれっぽく 動くところまではいったのでさっそく公開しておこう.

GDB対応だが,第10回の「いよいよGDB対応だ!」以降で いろいろ説明してあるが,スタブという簡単な応答プログラムを実装することになる. PCで起動したGDBからGDBプロトコルによってシリアルなどを経由して 「このレジスタの値を教えて」とか「このメモリの値を教えて」とか聞いてくるので, それに答えることになる.

スタブの実装例はGDBに以下のものが付属している.

i386-stub.c     m32r-stub.c     m68k-stub.c     sh-stub.c       sparc-stub.c
これらを適当にながめてH8用に移植すればいいわけだ.ということでsparc-stub.cあたりが シンプルな実装なのでそのへんを見てH8向けに stub.c を実装してみた.

修正点を説明しよう.

まずMakefileだが,stub.cをコンパイル対象にするのとデバッグオプション-gを 追加する修正を行っている.

diff -ruN h8_15/os/Makefile h8_16/os/Makefile
--- h8_15/os/Makefile	2011-12-29 16:18:49.000000000 +0900
+++ h8_16/os/Makefile	2011-12-29 17:54:46.000000000 +0900
@@ -24,12 +24,15 @@
 # for simulator
 OBJS += vector.o intr.o
 
+# for debugger
+OBJS += stub.o
+
 TARGET = kozos
 
 CFLAGS = -Wall -mh -nostdinc -nostdlib -fno-builtin
 #CFLAGS += -mint32 # intを32ビットにすると掛算/割算ができなくなる
 CFLAGS += -I.
-#CFLAGS += -g
+CFLAGS += -g
 CFLAGS += -Os
 #CFLAGS += -O0
 CFLAGS += -DKOZOS

次にcommand.cを修正して,debugコマンドを実行するとデバッグモードに入って 強制ブレークするようにしてある.あとブレークポイントのテスト用にcallという コマンドを実行するとfunc()というダミー関数を呼ぶような処理を追加.

diff -ruN h8_15/os/command.c h8_16/os/command.c
--- h8_15/os/command.c	2011-12-29 16:18:49.000000000 +0900
+++ h8_16/os/command.c	2011-12-29 17:48:54.000000000 +0900
@@ -4,6 +4,7 @@
 #include "timerdrv.h"
 #include "netdrv.h"
 #include "icmp.h"
+#include "stub.h"
 #include "lib.h"
 
 /* コンソール・ドライバの使用開始をコンソール・ドライバに依頼する */
@@ -51,6 +52,13 @@
   kz_send(MSGBOX_ID_ICMPPROC, 0, (char *)buf);
 }
 
+char *func(char *str)
+{
+  static char buf[32];
+  strcpy(buf, str);
+  return buf;
+}
+
 int command_main(int argc, char *argv[])
 {
   char *p;
@@ -78,6 +86,11 @@
     } else if (!strncmp(p, "ping", 4)) { /* pingコマンド */
       send_write("ping start.\n");
       send_icmp();
+    } else if (!strncmp(p, "debug", 5)) { /* デバッガ起動 */
+      set_debug_traps();
+      force_break();
+    } else if (!strncmp(p, "call", 4)) { /* ダミー関数の呼び出し */
+      send_write(func(p + 4));
     } else {
       send_write("unknown.\n");
     }
次に割り込みまわりの修正で,trapa #3 をブレークポイントとして拾えるように 割り込みベクタを追加する修正.これはブートローダー側に加える修正だが, 現在はシミュレータ対応のためにOS側も同じものを持っているので,OS側にも 同様に手を入れる.
diff -ruN h8_15/os/intr.S h8_16/os/intr.S
--- h8_15/os/intr.S	2011-12-29 16:18:49.000000000 +0900
+++ h8_16/os/intr.S	2011-12-29 16:25:07.000000000 +0900
@@ -131,3 +131,29 @@
 	mov.l	@er7+,er5
 	mov.l	@er7+,er6
 	rte
+
+	.global	_intr_bkpoint
+#	.type	_intr_bkpoint,@function
+_intr_bkpoint:
+	mov.l	er6,@-er7
+	mov.l	er5,@-er7
+	mov.l	er4,@-er7
+	mov.l	er3,@-er7
+	mov.l	er2,@-er7
+	mov.l	er1,@-er7
+	mov.l	er0,@-er7
+	mov.l	er7,er1
+	mov.l	#_intrstack,sp
+	mov.l	er1,@-er7
+	mov.w	#SOFTVEC_TYPE_BKPOINT,r0
+	jsr	@_interrupt
+	mov.l	@er7+,er1
+	mov.l	er1,er7
+	mov.l	@er7+,er0
+	mov.l	@er7+,er1
+	mov.l	@er7+,er2
+	mov.l	@er7+,er3
+	mov.l	@er7+,er4
+	mov.l	@er7+,er5
+	mov.l	@er7+,er6
+	rte
diff -ruN h8_15/os/intr.h h8_16/os/intr.h
--- h8_15/os/intr.h	2011-12-29 16:18:49.000000000 +0900
+++ h8_16/os/intr.h	2011-12-29 16:25:23.000000000 +0900
@@ -3,12 +3,13 @@
 
 /* ソフトウエア・割込みベクタの定義 */
 
-#define SOFTVEC_TYPE_NUM     5
+#define SOFTVEC_TYPE_NUM     6
 
 #define SOFTVEC_TYPE_SOFTERR 0
 #define SOFTVEC_TYPE_SYSCALL 1
 #define SOFTVEC_TYPE_SERINTR 2
 #define SOFTVEC_TYPE_TIMINTR 3
 #define SOFTVEC_TYPE_ETHINTR 4
+#define SOFTVEC_TYPE_BKPOINT 5
 
 #endif
diff -ruN h8_15/os/vector.c h8_16/os/vector.c
--- h8_15/os/vector.c	2011-12-29 16:18:49.000000000 +0900
+++ h8_16/os/vector.c	2011-12-29 16:24:26.000000000 +0900
@@ -6,6 +6,7 @@
 extern void intr_serintr(void); /* シリアル割込み */
 extern void intr_timintr(void); /* タイマ割込み */
 extern void intr_ethintr(void); /* イーサネット・コントローラ割込み */
+extern void intr_bkpoint(void); /* デバッガ用ブレークポイント */
 
 /*
  * 割込みベクタの設定.
@@ -13,7 +14,7 @@
  */
 void (*vectors[])(void) = {
   start, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 
-  intr_syscall, intr_softerr, intr_softerr, intr_softerr,
+  intr_syscall, intr_softerr, intr_softerr, intr_bkpoint,
   NULL, NULL, NULL, NULL, NULL, intr_ethintr /* IRQ5 */, NULL, NULL,
   NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
   NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
最後にstub.cについてだが,以下の点が注意.それ以外はsparcの実装とかを見て テキトーに実装しただけ. 動作なのだが以下の手順で確認できるので,興味のある人はやってみて. 今回は実機で確認した.あとシリアルを2本(コンソール用とGDB用)利用するので, シリアルポートが2つあるPCが必要(USBのシリアルアダプタでもOK). 以下はコンソールに /dev/cuaU0,GDBに/dev/cuaU1を使用した例. マイコンボードにシリアルが1本しかないので,コンソールとGDBで繋ぎかえたりして けっこう面倒なことになっている.
  1. H8用のGDBをビルドしておく.
  2. ブートローダーにも修正が入っているので,まずはブートローダーを新しいものに 上書きする.
  3. ターミナルアプリ(cuやTeraTerm)から /dev/cuaU0に接続し通常通り起動しOSをロード, OSを起動する.
  4. 「debug」コマンドを実行する.→ ブレークポイントのトラップ例外を拾うようになり, トラップ命令を実行して強制ブレークする.→ コンソールに「$T05#b9」とか出力される.
  5. もう一本のシリアル(/dev/cuaU1)のほうにマイコンボードをつなぎなおす.
  6. kozos.elfを引数にしてH8用のGDBを起動.
  7. GDBで「target remote /dev/cuaU1」を実行してシリアルに接続する.
  8. 関数force_break()でトラップ命令を呼ぶところで止まっているはず.
  9. GDBで「break func」を実行してブレークポイントを設定し「continue」でリスタート.
  10. マイコンボードをコンソール側(/dev/cuaU0)に接続しなおす.
  11. ターミナルアプリから「call」コマンドを実行する.
  12. マイコンボードをGDB側(/dev/cuaU1)に接続しなおす.
  13. func()でブレークしているはす.
ちなみに実は標準のGDBでは,func()でのブレークがうまくいかない. ここでちょい苦労したのだが,どうもH8用のGDBはブレークポイントを張ると, トラップ命令とかではなくスリープ命令を挿入するようなのだ.これはGDBプロトコルを 眺めると,ブレークポイントの設定の際に「0x0180」という値を書き込もうとしていて, このバイト列を逆アセンブルするとsleep命令だということがわかる.

謎なのでGDBのソースコードを追ってみる.grep breakpoint とかやって追っかけると 以下のようにどうも呼ばれているようだ.

そして h8300_breakpoint_from_pc() で以下のような部分があって,ここで sleep命令(0x0180)が指定されているようなのだ.
  /*static unsigned char breakpoint[] = { 0x7A, 0xFF }; *//* ??? */
  static unsigned char breakpoint[] = { 0x01, 0x80 };   /* Sleep */
ちなみに上の「0x7aff」というのは逆アセンブルすると「bpt」という命令になる. まんま「ブレークポイント」のことのように思える.でもH8のマニュアルとか見ても, bptという命令は出てこなくて謎.

これについてなのだが,どうもシミュレータで動作させるときのもののように思える. というのはGDBのH8シミュレータのソース(sim/h8300/compile.c)を見ると, スリープ命令やbpt命令のときにSIGTRAPを発行してブレークするような実装に思える (compile.cのO_BPTやO_SLEEPの部分).つまりbpt命令というのは実H8には存在しないが ブレークのためにGDB側で独自に定義している疑似命令なのではないかと思うのだ. またシミュレータではsleep命令があるとそこでブレークする,という動作のようだ. なのでブレークポイントにsleep命令を埋め込んでいるのだろう.

まあ理由はさだかではないがいずれにしてもGDBは標準ではブレークポイントに上記0x0180を 埋め込んでしまうので,スリープ命令が埋め込まれてしまい,そのままではブレークポイント がうまく動作しない.ということで上記 h8300_breakpoint_from_pc() を以下のように 書きかえることで trapa #3 を埋め込むようになる.この修正をしてgdbをリビルドすると ブレークポイントが使えるようになる.

--- h8300-tdep.c~	2011-03-19 03:52:30.000000000 +0900
+++ h8300-tdep.c	2011-12-27 16:43:01.000000000 +0900
@@ -1192,21 +1192,22 @@
   if (regno == E_EXR_REGNUM)
     return E_PSEUDO_EXR_REGNUM (gdbarch);
   return regno;
 }
 
 const static unsigned char *
 h8300_breakpoint_from_pc (struct gdbarch *gdbarch, CORE_ADDR *pcptr,
 			  int *lenptr)
 {
   /*static unsigned char breakpoint[] = { 0x7A, 0xFF }; *//* ??? */
-  static unsigned char breakpoint[] = { 0x01, 0x80 };	/* Sleep */
+  /*static unsigned char breakpoint[] = { 0x01, 0x80 };	*//* Sleep */
+  static unsigned char breakpoint[] = { 0x57, 0x30 };	/* trapa #0x3 */
 
   *lenptr = sizeof (breakpoint);
   return breakpoint;
 }
 
 static void
 h8300_print_float_info (struct gdbarch *gdbarch, struct ui_file *file,
 			struct frame_info *frame, const char *args)
 {
   fprintf_filtered (file, "\
もうひとつの策として,スタブ側で対策するという方法もある. たとえばGDBから0x0180を書き込むような指令がきたら,スタブはそれを書き込まずに 「0x0x5730」を書き込んでしまうというものだ.これだとGDB側を修正しなくて済むが, ほんとに「0x0180」という値を書き込みたいときに誤動作することになる.

まあとりあえずはスタブがそれっぽく動いた.でもまだステップ実行に対応していないので, ブレークして変数の値を見るくらいしかできないのだが,これだけでもデバッグ用には けっこう使えるかもしんない.


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