(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

前回は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 *)&registers[ESP], ptr, 4, 0); /* SP */
- *ptr++ = ';';
-
- *ptr++ = hexchars[EBP];
+ /* See gdb/regformats/reg-ppc.dat */
+ *ptr++ = hexchars[SP];
*ptr++ = ':';
- ptr = mem2hex((char *)&registers[EBP], ptr, 4, 0); /* FP */
+ ptr = mem2hex((char *)&registers[SP], ptr, 4, 0); /* SP */
*ptr++ = ';';

*ptr++ = hexchars[PC];
*ptr++ = ':';
ptr = mem2hex((char *)&registers[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(&registers[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], &registers[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, &param);
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については,今回改めていろいろ調べたり,再度いじったりしてわかってきたこともいろいろあって,ネタもいろいろ増えた.やっぱし自分でいろいろいじってみると,新しいことがいろいろとわかってくるもんだね.

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