(移植編第4回)GDB対応について説明しよう

2009/04/08

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

前回はPowerPC用KOZOSのGDB対応を行ったのだけど, 動作確認だけで,対応の内容を説明していなかった.ということで今回はGDB対応の ための修正内容の説明をしよう.

まずはGDBスタブについて.前回も書いたけど,スタブはGDBに付属しているi386用の ものをKOZOS用に拡張(スレッド対応などがされている)したもの(i386-stub.c)を ベースにして,PowerPC用に移植して使っている(ppc-stub.c). 実際に修正した内容は基本的に以下だ.

以下,i386-stub.cからppc-stub.cへの変更の差分から,ポイントになるところについて 説明しよう.

まず,バッファサイズについて.

/* BUFMAX defines the maximum number of characters in inbound/outbound buffers*/
/* at least NUMREGBYTES*2 are needed for register packets */
#if 0
#define BUFMAX 400
#else
#define BUFMAX (400*4)
#endif
まずこれは変更しているわけではないのだけど,重要なので説明.まあコメントに あるとおりなのだが,BUFMAXはスタブの送受信用のバッファサイズとなる. で,バッファのサイズなのだけど,レジスタ1個の内容を16進数の文字列にして 送信しようとすると,8文字が必要だ.

NUMREGBYTES というのは,レジスタの総サイズ(単位はバイト)になっている. なので,16進数の文字列にすると1バイト2文字が必要になる. ということで,レジスタのデータだけで最低でも NUMREGBYTES * 2 というサイズが 必要になるのだが,これにさらにコマンドとかチェックサムが数バイト付加される ので,余裕を持って定義する必要がある.

PowerPCの場合,GDBに通知が必要なレジスタの数は, GPRが32個,FPRが32個,その他のLRとかCRとかが6個になる. で,FPRは浮動小数用の64ビットレジスタなので, 総サイズは(32+6)*4+(32*8)=408バイトとなる(32ビットレジスタは4バイト, 64ビットレジスタは8バイトなので,4と8をかけている). 文字列にすると2倍して816バイト, で,バッファサイズは余裕をみて400*4=1600バイトとしている. この見積りが足りないと,スタブの処理で固まったりする. で,スタブ内にバグがあると非常にデバッグしにくいので注意が必要.

次にレジスタの定義について.

 /* Number of registers.  */
-#define NUMREGS	16
+#define NUMREGS	(32+2*32+6) /* GPR(32), FPR(32), SRR0/1, CR, CTR, LR, XER */
 
 /* Number of bytes of registers.  */
 #define NUMREGBYTES (NUMREGS * 4)
 
-enum regnames {EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI,
-	       PC /* also known as eip */,
-	       PS /* also known as eflags */,
-	       CS, SS, DS, ES, FS, GS};
+enum regnames {
+	/* See gdb/regformats/reg-ppc.dat */
+
+	GPR0,  GPR1,  GPR2,  GPR3,  GPR4,  GPR5,  GPR6,  GPR7,
+	GPR8,  GPR9,  GPR10, GPR11, GPR12, GPR13, GPR14, GPR15,
+	GPR16, GPR17, GPR18, GPR19, GPR20, GPR21, GPR22, GPR23,
+	GPR24, GPR25, GPR26, GPR27, GPR28, GPR29, GPR30, GPR31,
+
+	/* FPR * 32 */
+
+	SRR0 = (32+2*32), SRR1, CR, LR, CTR, XER
+};
+
+#define SP GPR1
+#define PC SRR0
+#define MSR SRR1
 
 /*
  * these should not be static cuz they can be used outside this module
  */
 int registers[NUMREGS];
スタブは registers[] という配列を持っていて,レジスタの値の保存に利用している. で,各レジスタの値はこの配列に決められた順番(enum regnames で定義された順番) で保存される.

PC上のGDBからスタブに対してレジスタの値の一覧の取得要求がきた場合には, この配列の内容がそのままGDBに送られる. ということは順番を間違えると,PCのGDB側で正しくレジスタの値が読めない.

で,問題はこの順番なのだが,GDBのソースの gdb/regformats というディレクトリ内の ファイルに書かれている.PowerPC の場合は gdb/regformats/reg-ppc.dat を 見ればよい.以下,reg-ppc.dat から抜粋.

32:r0
32:r1
32:r2
32:r3
...(中略)...
32:r30
32:r31

64:f0
64:f1
...(中略)...
64:f30
64:f31

32:pc
32:ps

32:cr
32:lr
32:ctr
32:xer
32:fpscr
まず先頭にGPR0〜31,次にFPR0〜31という順番になる. あとpcはおそらくプログラムカウンタのことなのでSRR0でいいと思う. psはよくわからんのだが,ステータスが格納されているSRR1(ステータスレジスタで あるMSRの値が割り込み発生時にSRR1に保存されているので)がどこかに必要な はずなのだがどこにも無いので,消去法でSRR1でいいのでは?と思う. (Processor Status で「ps」かな?)

FPSCRは浮動小数用のステータスレジスタなのだけど,なんかこれは無くても問題無い ようなので,とりあえず今回は省略(組み込みなので浮動小数使う予定も無いし). もしもGDB側で参照しているならば,領域だけは確保して,NUMREGS に計上しておくと いいかもしれない.

次に,ブレークポイントの定義を修正.

-#define BREAKPOINT() asm("   int $3");
+#define BREAKPOINT() asm("   trap");
PowerPCではトラップ命令というのがあって,実行するとトラップ例外が発生する. なのでそれに変更しておいた.

次に,割り込みベクタ番号からシグナル番号への変換方法の修正.

 int
 computeSignal (int exceptionVector)
 {
   int sigval;
   switch (exceptionVector)
     {
-    case 0:
-      sigval = 8;
-      break;			/* divide by zero */
-    case 1:
-      sigval = 5;
-      break;			/* debug exception */
-    case 3:
-      sigval = 5;
-      break;			/* breakpoint */
-    case 4:
-      sigval = 16;
-      break;			/* into instruction (overflow) */
     case 5:
-#if 0 /* これを変更しないとステップ実行がうまく動かない */
-      sigval = 16;
-#else
-      sigval = 5;
-#endif
-      break;			/* bound instruction */
-    case 6:
-      sigval = 4;
-      break;			/* Invalid opcode */
-    case 7:
-      sigval = 8;
-      break;			/* coprocessor not available */
-    case 8:
-      sigval = 7;
-      break;			/* double fault */
-    case 9:
-      sigval = 11;
-      break;			/* coprocessor segment overrun */
     case 10:
-      sigval = 11;
-      break;			/* Invalid TSS */
-    case 11:
-      sigval = 11;
-      break;			/* Segment not present */
-    case 12:
-      sigval = 11;
-      break;			/* stack exception */
-    case 13:
-      sigval = 11;
-      break;			/* general protection */
-    case 14:
-      sigval = 11;
-      break;			/* page fault */
-    case 16:
-      sigval = 7;
-      break;			/* coprocessor error */
+      sigval = exceptionVector;
+      break;
     default:
-      sigval = 7;		/* "software generated" */
+      sigval = 11;
+      break;
     }
   return (sigval);
 }
これは computeSignal() という関数の内部で,例外発生時のベクタ番号から それに対応するシグナル番号に変換している.

ターゲットボード側が(不正メモリアクセスなどで)例外処理に入った場合, スタブはPC上のGDBに対して,シグナル番号を通知する.これはUNIXの SIGBUSとかSIGSEGVとかの,いわゆるシグナルに対応している. というかUNIXのシグナル番号の仕様をそのまま流用している,というのが正しいか. たとえば不正メモリアクセスが発生した場合には,SIGBUSとして10という値が 通知される,という具合だ.で,GDB側ではこのシグナルの値によって, スタブ側でどのような現象によって例外が発生したか(不正メモリアクセスなのか, 不正命令実行なのか,など)を判断する.10が来たらSIGBUSなので, 不正メモリアクセスだと判断するわけだ.

なので computeSignal() では,ベクタ番号や各種ステータスレジスタを参照し, 例外が発生した原因を調べ,それに対応するシグナル番号を返す,ということを やらなければならない.まあよくわからなければとりあえずSIGBUSとして10あたりを 返しておけばとりあえずはいいのだけど,トラップ命令例外とかはきちんと見ないと, ステップ実行とかが正常に行えない原因になるだろう.

ちなみに送られてきたシグナル番号に対してGDBがどのように動作するかは, gdb で info signal とか info signals で参照できる. 設定は handle コマンドというので変更できるようだけど,よく知らない (まあ設定する必要も無いし).

で,KOZOSでの実装なのだけど,実はスタブに処理を渡す前に thread.c:thread_intr() で似たようなこと(ベクタ番号からシグナル番号への変換)をやっていて, 変更後のシグナル番号がスタブに渡されてくるようになっている.なので基本的には computeSignal()は引数をそのまま返してやればよい. よくわからんものに関しては,とりあえずSIGSEGVとして11を返すようにしておいた.

ただ,これはスタブの流儀からするとちょっとイマイチなので, スタブを他のPowerPC機器に移植して利用することなどを考えると,将来的には ベクタ番号とステータスレジスタとかからシグナル番号を判断するように修正したい.

次に,例外発生時のGDBへの通知方法について.

   /* reply to host that an exception has occurred */
   sigval = computeSignal (exceptionVector);
 
   ptr = remcomOutBuffer;
 
   *ptr++ = 'T';			/* notify gdb with signo, PC, FP and SP */
   *ptr++ = hexchars[sigval >> 4];
   *ptr++ = hexchars[sigval & 0xf];
 
-  *ptr++ = hexchars[ESP]; 
-  *ptr++ = ':';
-  ptr = mem2hex((char *)®isters[ESP], ptr, 4, 0);	/* SP */
-  *ptr++ = ';';
-
-  *ptr++ = hexchars[EBP]; 
+  /* See gdb/regformats/reg-ppc.dat */
+  *ptr++ = hexchars[SP]; 
   *ptr++ = ':';
-  ptr = mem2hex((char *)®isters[EBP], ptr, 4, 0); 	/* FP */
+  ptr = mem2hex((char *)®isters[SP], ptr, 4, 0);	/* SP */
   *ptr++ = ';';
 
   *ptr++ = hexchars[PC]; 
   *ptr++ = ':';
   ptr = mem2hex((char *)®isters[PC], ptr, 4, 0); 	/* PC */
   *ptr++ = ';';
例外発生時には computeSignal() によってベクタ番号から例外の内容を示す シグナル番号に変換し,TコマンドというのによってGDB側に通知する. この際に,Tの後にシグナル番号が付加され,さらに重要なレジスタの値が 簡易的に付加される.GDBはシグナル番号を見てその後の動作を決定するわけだが, このときいっしょに最低限のレジスタの値を渡しておくことで,迅速に動作できる というわけだ.(でも実際には結局のところgコマンドでレジスタの値をごっそりと ロードすることが多いようなのだけど)

で,ここで付加しなければならないレジスタはやっぱりCPUごとに決まっているの だけど,これもどうやら gdb/regformats/reg-ppc.dat に書いてあるようだ.

以下,reg-ppc.dat から抜粋.

name:ppc
expedite:r1,pc
r1とあるが,PowerPCではGPR1はスタックポインタとなるので,スタックポインタの値と PC(プログラムカウンタ)の値を付加すればいいようだ. なので,そのように修正している.ちなみにSPは実際にはGPR1,PCは実際には SRR0となるように以下のように定義されている.
#define SP GPR1
#define PC SRR0
#define MSR SRR1
次に,ステップ実行時のレジスタ設定について.
 	case 's':
 	  stepping = 1;
 	case 'c':
 	  /* try to read optional parameter, pc unchanged if no parm */
 	  if (hexToInt (&ptr, &addr))
 	    registers[PC] = addr;
 
 	  newPC = registers[PC];
 
 	  /* clear the trace bit */
-	  registers[PS] &= 0xfffffeff;
+	  registers[MSR] &= MSR_SE;
 
 	  /* set the trace bit if we're stepping */
 	  if (stepping)
-	    registers[PS] |= 0x100;
+	    registers[MSR] |= MSR_SE;
 
 #if 0
 	  _returnFromException ();	/* this is a jump */
 #else
 	  return;
 #endif
 	  break;
ステップ実行時には,GDB側からsコマンドというのが来る. continue の場合はcコマンドというのが来る.

たいていのCPUはステップ実行機能というのを持っていて,特定のフラグを立てて おくと,アセンブラで1命令を実行するたびに割り込みを発生させてくれる.

で,sコマンドがきたら,ステップ実行のためにステップ実行フラグを立てなければ ならない.このフラグはもちろんCPU依存だが,PowerPCはMSRにそーいうビットを持って いるので,それを上げ下げするように修正.

以上がデバッグスタブ(ppc-stub.c)の修正になる.

次に,stublib.c の修正について.

もともと以前にも stublib.c というのがあって,OS側からスタブを呼び出す際の レジスタ保存処理と,あとシリアルの1文字送受信のライブラリ (putDebugChar()/getDebugChar() という関数で,スタブの移植時にはこれを実装する 必要がある.詳しくは第10回を参照)が置いてあった.

で,以下のように修正.stublib.c は変更点が多いので,差分でなくファイルの内容を そのまま添付してある.

void putDebugChar(int c)
{
  serial_putc(SERIAL_NUMBER, c);
}

int getDebugChar()
{
  return serial_getc(SERIAL_NUMBER);
}
まずシリアル送受信の putDebugChar()/getDebugChar() だけど, 今回はシリアル通信用のライブラリ(serial.c)があるので,それを使って 1文字送受信するように修正した.

次にレジスタの値の保存と復旧.

void stub_store_regs(kz_thread *thp)
{
  memset(registers, 0, sizeof(registers));

  /* 注意:grp0,gpr1は逆に格納されている */ 
  registers[GPR1] = thp->context.gpr[0];
  registers[GPR0] = thp->context.gpr[1];
  memcpy(®isters[GPR2], &thp->context.gpr[2], 4*30);

  registers[PC]  = thp->context.pc;
  registers[MSR] = thp->context.msr;
  registers[CR]  = thp->context.cr;
  registers[LR]  = thp->context.lr;
  registers[CTR] = thp->context.ctr;
  registers[XER] = thp->context.xer;
}

void stub_restore_regs(kz_thread *thp)
{
  thp->context.gpr[0] = registers[GPR1];
  thp->context.gpr[1] = registers[GPR0];
  memcpy(&thp->context.gpr[2], ®isters[GPR2], 4*30);

  thp->context.pc  = registers[PC];
  thp->context.msr = registers[MSR];
  thp->context.cr  = registers[CR];
  thp->context.lr  = registers[LR];
  thp->context.ctr = registers[CTR];
  thp->context.xer = registers[XER];
}
これも従来はi386用に書いてあったけど,PowerPC用に修正.

最後に,OSからスタブに処理を渡す際に呼び出す関数について.

int stub_proc(kz_thread *thp, int signo)
{
  gen_thread = thp;

  stub_store_regs(gen_thread);

  clearDebugChar();
  handle_exception(signo);

  stub_restore_regs(gen_thread);

  return 0;
}
OS側では,不正メモリアクセスなどが発生した場合には,この stub_proc() を 呼び出すことになる.stub_proc() ではレジスタの値を registers[] に保存して, スタブの処理関数(handle_exception())を呼び出す.で,処理が終ったら レジスタの値を復旧して戻る.

ちなみに getDebugChar() ではビジーループでGDBからの指示を待つので, GDB側からcontinueなどの通知が来ない限りは,OSに戻らずにずっと待ち続ける ことになる.つまりGDBに処理が渡っている間は,実機はウンともスンとも反応しない, ということになる.(これはスタブの正しい動作です)

あと,OSのスタート時にスタブ処理を有効にする処理を追加.

 int mainfunc(int argc, char *argv[])
 {
+  kz_debug(stub_proc);
+
   extintr_id  = kz_run(extintr_main,  "extintr",   1, 0, NULL);
   idle_id     = kz_run(idle_main,     "idle",     31, 0, NULL);
   command0_id = kz_run(command_main,  "command0", 11, 0, NULL);
+#if 0
   command1_id = kz_run(command_main,  "command1", 11, 1, NULL);
+#endif
 
   return 0;
 }

最後に,kz_debug()システムコールをちょっと修正. 従来はデバッグ用のソケット番号を引数にしていたが, デバッグ処理時のコールバック関数を引数にするように変更した.

-int kz_debug(int sockt)
+int kz_debug(kz_dbgfunc func)
 {
   kz_syscall_param_t param;
-  param.un.debug.sockt = sockt;
+  param.un.debug.func = func;
   kz_syscall(KZ_SYSCALL_TYPE_DEBUG, ¶m);
   return param.un.debug.ret;
 }
-static int thread_debug(int sockt)
+static int thread_debug(kz_dbgfunc func)
 {
-  debug_sockt = sockt;
-#if 0
-  stub_init(sockt);
-#endif
+  debug_handler = func;
+  stub_init();
   putcurrent();
   return 0;
 }
   case SIGSEGV:
   case SIGTRAP:
   case SIGILL:
-    if (debug_sockt) {
-#if 0
-      stub_proc(thp, signo);
-#endif
+    if (debug_handler) {
+      debug_handler(thp, signo);
     } else {
       /* ダウン要因発生により継続不可能なので,スリープ状態にする*/
       getcurrent();
あーつかれた.説明は以上なのだけど,わかるかしら?

まあGDBスタブって,構造は非常に単純なのできちんと読めばわからないことは無いとは 思うのだけど,資料も少ないし,グレーな部分というかきちんと決まっていないような 部分も多く,実装がすべてみたいなところもある.なのでこのような実装と詳細解説は けっこういろんなところで必要とされているのでは?と思って書いてみたが どうでしょう.これくらいの修正ならば,新しいCPUに対応するのもそんなに大変では ないと思えるんではなかろうか.

まあGDBについては,今回改めていろいろ調べたり,再度いじったりしてわかってきた こともいろいろあって,ネタもいろいろ増えた.やっぱし自分でいろいろいじってみる と,新しいことがいろいろとわかってくるもんだね.

なので,ちょっと今後もいろいろ書いていこうと思う.


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