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

最近,プロファイラについてちょっとしたアイディアが出たので,今回はプロファイラの機能を使ってデバッグ用のログ出力の埋め込みというのをやってみよう.

まずはプロファイラの説明だけど,プロファイラって積極的に使っているひとはなにげにあまりいなかったりするものだけど,gccの場合だと簡単にいうとこんなことができる.
  • コンパイル時に -pg をo付けてコンパイルする.
  • 実行形式を実行すると,どの関数が何回呼ばれたとか,どの関数がどれくらい時間をくっているかといったデータがとれる.
まあ使いかたについては「UNIX プログラミングの道具箱」にザックリ書いてあるので,興味のあるかたは参照してほしい.見てわかるとおり,プログラム中で重い部分(ホットスポットという)を見つけてチューニングするための情報収集につかうものなのだけど,今回はこの -pg の機能を利用して,ちょっと面白いことをやってみよう.

-pgつけてコンパイルすると実際には何が行われるかというと,関数の先頭に必ず特定の関数呼び出しを行うようなコードが付加される.実際にプロファイラを動かしたときには,すべての関数の先頭で特定の処理(統計を記録するような)が行われるわけだ.実際にどんな処理が行われるかはちょっとよくわかっていないのでここでは説明を省くが,今回利用するのはこの「関数の先頭に,特定の関数を呼び出すようなコードを付加する」という機能だ.

ためしに -pg をつけてコンパイルしてみよう.

まず,ふつうにコンパイルした場合を見てみる.-S をつけてコンパイルすることで,アセンブラコードとして main.s が作成される.

% /usr/local/powerpc-linux/bin/gcc -c -S main.c -Wall -nostdinc -fno-builtin -msoft-float -I../uboot/u-boot.git/include -g -DKOZOS

これで作成された main.s の中身を見てみる.

.globl main
.type main, @function
main:
.LFB6:
.loc 1 94 0
stwu 1,-32(1)
.LCFI13:
mflr 0
stw 31,28(1)
.LCFI14:
stw 0,36(1)

これは main() 関数の先頭部分なのだけど,関数の先頭ではまず mflr でGPR0 にLRの値を保存している.で,stwu で GPR1 の値を減算することでスタックを確保して,スタックに(GPR0にコピーした)LRの値を保存している.

つぎに -pg をつけて同じようにコンパイルしてみよう.

% /usr/local/powerpc-linux/bin/gcc -c -S main.c -Wall -nostdinc -fno-builtin -msoft-float -I../uboot/u-boot.git/include -g -DKOZOS -pg

お尻に -pg がついていることに注目.で,同じように作成された main.s を見てみる.

.globl main
.type main, @function
main:
.LFB6:
.loc 1 94 0
.section ".data"
.align 2
.LP6:
.long 0
.section ".text"
mflr 0
lis 12,.LP6@ha
stw 0,4(1)
la 0,.LP6@l(12)
bl _mcount
stwu 1,-32(1)
.LCFI16:
mflr 0
stw 31,28(1)
.LCFI17:
stw 0,36(1)

上は同じ main() 関数の先頭部分なのだけど,stwu によるスタック確保の前に,いくつかの命令が追加されている.

まずはスタック確保の直前で _mcount という関数が呼ばれていることに注目してほしい.これが,プロファイリングを行うための関数呼び出しだ.

で,この _mcount() を自前で用意してやることで,すべての関数呼び出し時に特定の処理を入れることができる.たとえば関数呼び出しのログを保存するとかだ.これはデバッグに非常に役に立つのではなかろうか.

実際に埋め込まれた _mcount() 呼び出し処理をよくみてみよう.LRの値をGPR1 + 4の位置に保存してから _mcount() を呼んでいる._mcount() から戻った後は,即座にLRの値をスタックに保存しているから,LRの値はきちんと設定した上で _mcount() から戻ってこないといけないようだ.

なので,_mcount() はこんなふうに書けばいいんではなかろうか.

.text
.globl _mcount
.type _mcount,@function
_mcount:
stwu 1,-48(1)
stw 3,16(1)
stw 4,20(1)
stw 5,24(1)
stw 6,28(1)
stw 7,32(1)
stw 8,36(1)
stw 9,40(1)
stw 10,44(1)
mflr 3
stw 3,8(1)
bl logging

lwz 9,8(1)
mtctr 9
lwz 10,52(1)
mtlr 10
lwz 3,16(1)
lwz 4,20(1)
lwz 5,24(1)
lwz 6,28(1)
lwz 7,32(1)
lwz 8,36(1)
lwz 9,40(1)
lwz 10,44(1)
addi 1,1,48
bctr

_mcount() ではレジスタの値を一時的にスタックに保存して logging() を呼び出し,レジスタの値を復旧して呼び出し元に戻っている.ちなみに戻るときはLRの値を設定した状態で戻らなければならない(呼び出し元では,LRが差す先に戻ろうとするから)ので,戻るのにblrは使えない.ということで,LRの値を設定した後にbctrで戻っている.

問題はレジスタの退避なのだけど,厳密にいえばlogging()で利用しているレジスタのみ保存すればよい.というのは,ここで保存しているのは,関数呼び出しの先頭で _mcount() が呼ばれた際に,_mcount() の処理でレジスタの値が書き変わってしまうと,レジスタ経由で渡された本来の引数の値が破壊されてしまうからだ.

PowerPCのABIでは,GPR3~GPR10を引数渡しに利用することになっている.なのでさらに厳密には,logging()内部でGPR3~GPR10のレジスタを利用している場合には,それだけ保存すればよい.だけどこれは logging() を書き換えるたびにどのレジスタを利用しているか調べる必要が出てしまって面倒なので,GPR3~GPR10の全部を保存するようにしている.

で,作成したのが以下のコード.

_mcount() はアセンブラで書く必要があるので,mcount.s というファイルを追加した.あと logging の実体として logging.h, logging.c が追加してある.

logging() は特定メモリ領域に関数のアドレスを保存するように,以下のような感じで書いてみた.

int logging_disable = 0;
int logging_cur = 0;
int logging_buf[LOGGING_BUF_SIZE];

void logging(int addr)
{
if (!logging_disable) {
logging_buf[logging_cur++] = addr;
if (logging_cur >= LOGGING_BUF_SIZE)
logging_cur = 0;
}
}

これで,_mcount() の呼び出しもとのアドレスが logging_buf[] に保存される._mcount() を呼び出すのは,-pg によって先頭で _mcount() を呼び出すようにコード付加された関数なので,-pg つけてコンパイルしたソースコードの関数が実行されると,ログが logging_buf[] に保存されるということになる.

で,command.c にコマンド追加した.「log」コマンドを実行すると,現在保存されているログが表示される.

} else if (!strncmp(buffer, "log", 3)) {
logging_disable = 1;
log = logging_cur;
for (i = 0; i < LOGGING_BUF_SIZE; i++) {
ub_printf("%d %08x\n", i, logging_buf[log++]);
if (log >= LOGGING_BUF_SIZE)
log = 0;
}
logging_disable = 0;

Makefile は特定のファイルのみ -pg 付きでコンパイルされるように修正.とりあえず今回は extintr.c を対象にしてみた.

diff -ruN porting05/Makefile porting06/Makefile
--- porting05/Makefile Sun Apr 12 12:09:59 2009
+++ porting06/Makefile Sun Apr 19 21:53:32 2009
@@ -6,6 +6,7 @@
OBJS += extintr.o
OBJS += kozos.o command.o
OBJS += stublib.o ppc-stub.o
+OBJS += mcount.o logging.o

LIB = libc.a
TARGET ?= sample
@@ -26,6 +27,9 @@

$(TARGET) : $(OBJS) $(LIB)
$(CC) $(OBJS) -o $(TARGET) $(CFLAGS) $(LFLAGS)
+
+extintr.o : extintr.c
+ $(CC) -c $< $(CFLAGS) -pg

.c.o : $<
# $(CC) -S $< $(CFLAGS)



ためしに実行してみよう.まずビルドしてファームをボードにダウンロードする.で,プロンプトでおもむろに log コマンドを実行してみる.

> echo aaa
aaa
OK
> log
000430e4 00043268 00043508 000430e4 0004314c 000430e4 00043268 00043508
000430e4 0004314c 000430e4 00043268 00043508 000430e4 0004314c 000430e4
00043268 00043508 000430e4 0004314c 000430e4 00043268 00043508 000430e4
0004314c 000430e4 00043268 00043508 000430e4 0004314c 000430e4 00043268
00043508 000430e4 0004314c 000430e4 00043268 00043508 000430e4 0004314c
000430e4 00043268 00043268 00043268 00043508 000430e4 0004314c 000430e4
00043268 00043508 000430e4 0004314c 000430e4 00043268 00043508 000430e4
0004314c 000430e4 00043268 00043508 000430e4 0004314c 000430e4 00043268
OK
>

これは extintr.c 内の関数が呼び出されたシーケンスになる.なんかそれっぽく表示されている.どうも 0x00043xxx 付近に集中しているようだ.

まあシーケンスとはいっても16進のアドレスだけではわからんので,実際に関数のアドレスを確認してみよう.関数がどのアドレスにマッピングされているかは,readelf で確認できる.

% readelf -a sample | grep FUNC | sort -k 2

で,0x00043xxx 付近を取り出したら以下のようになった.

90: 00043000 104 FUNC LOCAL DEFAULT 1 cons_mask
91: 00043068 104 FUNC LOCAL DEFAULT 1 cons_unmask
92: 000430d0 104 FUNC LOCAL DEFAULT 1 cons_checkintr
93: 00043138 284 FUNC LOCAL DEFAULT 1 cons_intr
94: 00043254 348 FUNC LOCAL DEFAULT 1 cons_command
95: 000433b0 212 FUNC LOCAL DEFAULT 1 cons_init
96: 00043484 112 FUNC LOCAL DEFAULT 1 extintr_intr_regist
87: 000434f4 208 FUNC LOCAL DEFAULT 1 extintr_handler
97: 000435c4 240 FUNC LOCAL DEFAULT 1 extintr_mainloop
305: 000436b4 148 FUNC GLOBAL DEFAULT 1 extintr_main

どれも extintr.c の関数なので,どうやらそれっぽく動いているようだ.ログの先頭の8個のアドレスのみ,実際の関数に置き換えてみた.

000430e4 → cons_checkintr()
00043268 → cons_command()
00043508 → extintr_handler()
000430e4 → cons_checkintr()
0004314c → cons_intr()
000430e4 → cons_checkintr()
00043268 → cons_command()
00043508 → extintr_handler()

割り込みが発生して extintr_handler() が呼ばれ,その先で checkintr() とintr() によって実際に割り込み処理が行われているように見える.あと command スレッドからのコマンドを受け付けて cons_command() がときどき呼ばれているようだ.うーん,まあまともに動いているように思える.

これのいいところは,-pgつけるだけで簡単にプログラム全体に処理を入れられることだ.ディレクトリ単位でログを入れたいときも,Makefileの修正で簡単に対応できる.

あと,_mcount()の処理は所詮関数呼び出しなので,スレッドの動作順番などが変わるようなことはない.つまり -pg を追加したことで,(処理は多少重くなるだろうが)動作シーケンスが変わってしまうなことはない(はずだ).これはデバッグのときには非常に重要なことた.というのは,実際の現場では,デバッグのためにログ保存を入れたらスレッドの動作順序が変わってしまってバグが発生しなくなってしまう,ということが起こり得る(で,ログ保存を外すとやっぱりバグは起きる).こーいうことを防止するためにも,ログ保存はスレッド動作に影響を与えないことがのぞましい(関数呼び出しなどで多少処理が重くなるのはしかたがないが).たとえばログ出力用やコンソール出力用に専用のスレッドがあるような場合は要注意だ.ログ出力を行うことでログ出力スレッドが動作してしまい,スレッドのスケジューリング順序が変わってしまうことで,タイミング的なバグなどは発生しなくなったり現象が変わってしまったりすることが考えられるからだ.

まあただすべての関数に処理が入ってしまうので,mcount()の先でうかつな処理をすると,動作がすげー重くなってしまう可能性はある.

今回はログ保存を実装したけど,他にもアイディア次第でこれってけっこういいアイディアだと思うのだけどどうでしょう.
Interfaceの記事を書いている真最中で,KOZOSの改良に手がまわらない...
で,記事なのだけど,PowerPC移植コードをベースにして,雑誌掲載用にごっそりと機能をそぎおとしてシンプルにしてみたのだけど,なんとコア部分で700行を切るまでになった.

hiroaki@teapot:~/tb0286/src7>% cat configure.h kozos.h thread.[ch] memory.[ch] syscall.[ch] extintr.c | wc -l
696
hiroaki@teapot:~/tb0286/src7>%

うーんすごいなあ.OSってたったのこれだけでも動いちゃうものなのだ.

まあファイルシステムも無いしソケットインタフェースも無いし仮想メモリも無いわけだけど,コンピュータの3大構成要素(CPU,メモリ,I/O)を管理できていれば,それでOSと言っていいと個人的には思っている.つまりスレッドの実現とメモリ管理,デバイス管理,割り込み管理くらいができてればもうそれでOS,といっていいと思う.

個人的には,世間的にはOS自作ってなんか無駄に敷居が高く感じられている気がするのだけど,KOZOSでなんとかそのへんのイメージを払拭したいものだ.

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

前回まででGDBスタブの移植と説明をしたのだけど,実はバグだらけでステップ実行とかがまともに動かないことが判明.

ということで,今回はごっそりとバグ修正.以下の修正を行った.
  • レジスタにFPSCRを追加.(実害無し)
  • トラップ命令でのブレーク時にステップ実行できない問題を修正.
  • ステップ実行フラグの操作ミスでMSRが不正になる問題を修正.(致命的バグ)
  • シリアルでウエイトをおくように修正.
  • 割り込みハンドラからの関数呼び出しで,LRの値によってGPR0が破壊される 問題の修正.(致命的バグ)
  • デバッグ用シリアルをPSC1→PSC2に変更.
  • スタブの処理終了時に,命令キャッシュをクリアする処理を追加.(致命的バグ)
  • ステップ実行時の割り込みラッチを追加.
致命的なバグが3つあって,これらは直しておかないと,まったくまともに動かない.あとは他にも気がついたところをちょこちょこ修正した.で,以下が修正済ソースコード.前回からの差分はいつもどおり diff.txt 参照.

で,以下に修正内容を説明する.

まず,ppc-stub.c について.浮動小数レジスタについてとくにサポートしていないので,FPSCRはGDBに渡さずにいたが,いちおう値だけは渡すように修正.(といってもゼロを渡すだけだけど)

diff -ruN porting03/ppc-stub.c porting05/ppc-stub.c
--- porting03/ppc-stub.c Tue Apr 7 23:21:40 2009
+++ porting05/ppc-stub.c Sun Apr 12 12:09:59 2009
@@ -119,7 +119,7 @@
static const char hexchars[]="0123456789abcdef";

/* Number of registers. */
-#define NUMREGS (32+2*32+6) /* GPR(32), FPR(32), SRR0/1, CR, CTR, LR, XER */
+#define NUMREGS (32+2*32+7) /* GPR(32), FPR(32), SRR0/1, CR, CTR, LR, XER, FPSCR */

/* Number of bytes of registers. */
#define NUMREGBYTES (NUMREGS * 4)
@@ -132,9 +132,9 @@
GPR16, GPR17, GPR18, GPR19, GPR20, GPR21, GPR22, GPR23,
GPR24, GPR25, GPR26, GPR27, GPR28, GPR29, GPR30, GPR31,

- /* FPR * 32 */
+ FPRS, /* FPR * 32 */

- SRR0 = (32+2*32), SRR1, CR, LR, CTR, XER
+ SRR0 = (32+2*32), SRR1, CR, LR, CTR, XER, FPSCR
};

#define SP GPR1

NUMREGSの数が増えたので,これでレジスタ一覧を渡す際にFPSCRの値も渡すようになる.まあこれは修正しなくても問題なさそうだったのだけどいちおう直しておく.

次に,trap命令によるブレーク時に,ステップ実行ができない問題を修正.

従来は kz_trap() とか kz_break() の内部では trap 命令を直接実行することでトラップ割り込みを上げてブレークしていた.しかしこれでブレークすると,GDBから step や next でステップ実行した際に,またその命令でブレークしてしまって永久に先に進めない.

ということで,トラップ命令で強制ブレークした場合には,PCの値を加算して次の命令から実行するように修正する.

@@ -190,7 +190,12 @@
that trace flag works right. */
asm(" iret");

-#define BREAKPOINT() asm(" trap");
+static int breaking = 0;
+#define BREAKPOINT() { \
+ breaking++; \
+ asm(" trap"); \
+ asm(" nop"); \
+ }

/* Put the error code here just in case the user cares. */
int gdb_i386errcode;
@@ -780,12 +785,18 @@
/* reply to host that an exception has occurred */
sigval = computeSignal (exceptionVector);

+ if (breaking && (sigval == 5) && (registers[MSR] & (1<<17))) {
+ breaking--;
+ registers[PC] += 4;
+ }
+
ptr = remcomOutBuffer;

trap命令の後には,いちおうnopを入れておいた.あとMSRの特定のビットを見ることでトラップ命令による割り込みかどうかを判断できるので,そーいうふうに修正した.このへんは,詳しくは「32ビット PowerPC アーキテクチャ プログラミング環境」(freescaleのホームページから日本語版PDFをダウンロードできる)の例外処理のあたりを参照してほしい.

次に,これは修正が必要かどうかちょっと迷ったのだが,例外発生時にPC上のGDBにブレークしたことを通知する部分で,レジスタの値を通知しないように修正する.

*ptr++ = 'T'; /* notify gdb with signo, PC, FP and SP */
*ptr++ = hexchars[sigval >> 4];
*ptr++ = hexchars[sigval & 0xf];

+#if 0
/* See gdb/regformats/reg-ppc.dat */
*ptr++ = hexchars[SP];
*ptr++ = ':';
@@ -796,6 +807,7 @@
*ptr++ = ':';
ptr = mem2hex((char *)&registers[PC], ptr, 4, 0); /* PC */
*ptr++ = ';';
+#endif

strcpy(ptr, "thread:");
ptr += 7;

なぜこのような修正をするかというと,実はここでは hexchars[PC] を見ている部分がある(上の差分には出てないけど)のだけど,PCの値は96なのでオーバーフローしている.これはまずい.

で,2桁にすべきかと思ってためしに

- *ptr++ = hexchars[PC];
+ *ptr++ = hexchars[PC >> 4];
+ *ptr++ = hexchars[PC & 0xf];

のようにしてみたのだけど,これだと値が大きすぎるといってGDBがエラーにしてしまうのだな.

で,レジスタの値を送信しなくても,GDBがgコマンドでレジスタ一覧を取得しにくるのでとくに問題はなさそうなので,レジスタの値は送信しないように修正した.まあそれで問題無く動いているので,これでよしとしよう.

次に,MSRのフラグの落とし間違いのしょぼいミスを修正.

@@ -911,11 +923,11 @@
newPC = registers[PC];

/* clear the trace bit */
- registers[MSR] &= MSR_SE;
+ registers[MSR] &= ~(MSR_SE /* | MSR_BE */);

/* set the trace bit if we're stepping */
if (stepping)
- registers[MSR] |= MSR_SE;
+ registers[MSR] |= (MSR_SE /* | MSR_BE */);

#if 0
_returnFromException (); /* this is a jump */

MSR_SEでandしてしまっていた.ああ,しょぼい.

MSR_BEはブランチ命令(ジャンプ命令のこと)でブレークするかというフラグなのだけど,試してみたらMSR_SEを立てておけばブランチ命令でも常にブレークするので,MSR_BEを立てる必要は無いみたい.いちおうコメントとして残しておいた.

次に serial.c について,シリアルまわりの修正.

diff -ruN porting03/serial.c porting05/serial.c
--- porting03/serial.c Tue Apr 7 23:21:40 2009
+++ porting05/serial.c Sun Apr 12 12:09:59 2009
@@ -78,6 +78,14 @@
return psc->psc_status & PSC_SR_RXRDY;
}

+static void udelay(int usec)
+{
+ volatile int i;
+ /* とりあえずてきとうな回数でのダミーループでウエイトを置く */
+ for (i = 0; i < (save_clk / 1000000) * usec; i++)
+ ;
+}
+
void serial_putc(int index, char c)
{
volatile struct mpc5xxx_psc *psc = regs[index].psc;
@@ -88,6 +96,12 @@
while (!(psc->psc_status & PSC_SR_TXEMP)) {
/* waiting */
}
+
+ /*
+ * フロー制御が無い場合のバッファ溢れ防止として,ウエイトを置く.
+ * データの取りこぼしが発生しているようなら,ウエイトを増やすこと.
+ */
+ udelay(20);

psc->psc_buffer_8 = c;
}

フロー制御が効いていないと,データ量が多くなったときに受けきれなくてとりこぼしが発生する可能性がある.なので,あまりガツガツとデータを送らないように,ウエイトを入れてみた.まあほんとうはクロックとかウエイトの値をきちんと計算して入れるべきなのだけど,とりこぼしが起きてるようならウエイトを増やすということで,不正確でもいいのでとりあえず入れておいた.なので名前は udelay() だけど,正確にマイクロ秒というわけではなくあくまで目安.はじめは udelay(1) くらいでやっていたのだけど,これだとどうも動作が不安定な場合が多く,udelay(10)くらいで安定した.なのでいちおうウエイトは20としてある.

ちなみにデータのとりこぼしが起きているかどうかは,

(gdb) set debug target 1
(gdb) set debug remote 1
(gdb) set debug serial 1

とかでデータを直接見ることで調べることができる.実際にGDBを使っているときにとりこぼしが発生すると,レジスタの値がきちんととれなかったりして,info registers とかやってもゼロばっかになっていたり,よくわからん止まりかたをしたりする.こんなときはウエイトの数を増やしてやる.

次に,startup.s について.LRの値によってGPR0が破壊されていた重大な問題の修正.

diff -ruN porting03/startup.s porting05/startup.s
--- porting03/startup.s Tue Apr 7 23:21:40 2009
+++ porting05/startup.s Sun Apr 12 12:09:59 2009
@@ -22,23 +22,22 @@
ori 1,1,0x200000@l
stwu 1,-160(1)

- stmw 2,8(1)
- stw 0,4(1)
+ stmw 0,8(1)
mfsprg2 2
- stw 2,0(1)
+ stw 2,12(1)

mflr 2
- stw 2,128(1)
+ stw 2,136(1)
mfcr 2
- stw 2,132(1)
+ stw 2,140(1)
mfctr 2
- stw 2,136(1)
+ stw 2,144(1)
mfxer 2
- stw 2,140(1)
+ stw 2,148(1)
mfsrr0 4
- stw 4,144(1)
+ stw 4,152(1)
mfsrr1 5
- stw 5,148(1)
+ stw 5,156(1)

bl 1f
1: mflr 3
@@ -54,25 +53,26 @@
.type dispatch,@function
dispatch:
mr 1,3
+ addi 1,1,-8

return_from_interrupt:
- lwz 2,128(1)
+ lwz 2,136(1)
mtlr 2
- lwz 2,132(1)
+ lwz 2,140(1)
mtcr 2
- lwz 2,136(1)
+ lwz 2,144(1)
mtctr 2
- lwz 2,140(1)
+ lwz 2,148(1)
mtxer 2
- lwz 2,144(1)
+ lwz 2,152(1)
mtsrr0 2
- lwz 2,148(1)
+ lwz 2,156(1)
andi. 2,2,0xffff
mtsrr1 2

- lmw 2,8(1)
- lwz 0,4(1)
- lwz 1,0(1)
+ lmw 2,16(1)
+ lwz 0,8(1)
+ lwz 1,12(1)

sync
isync

割り込み発生時にはレジスタの値をスタックに積むのだけれど,関数呼び出し時には,LRの値は前のスタックフレームに対して保存される.たとえば,以下を見てほしい.

00043cec :
43cec: 94 21 ff e0 stwu r1,-32(r1)
43cf0: 7c 08 02 a6 mflr r0
43cf4: 93 e1 00 1c stw r31,28(r1)
43cf8: 90 01 00 24 stw r0,36(r1)
...

上は command_main のアセンブル結果だが,スタックポインタ(GPR1)の値を32バイト引くことでスタックを32バイト確保し,その後GPR31の値はGPR1 + 28 の位置(つまり,このスタックフレームのお尻)に保存しているのだが,LRの値は GPR1 + 36 の位置に保存している.つまり,前のスタックフレームの中に保存しているわけだ.

このためスタックフレームの先頭を空けておかなければならないのだが,従来の _intr ではスタックフレームの先頭からレジスタの値を保存していた.で,ここにはGPR0の値が保存されていたため,その後の関数呼び出しでLRの値に上書きされてしまい,GPR0が使われている場合に誤動作していた.こーいうレジスタの保存ミスって,非常に見つけにくいバグの原因になるんだよね.(実際,非常に見つけにくかった)

ちなみにこのスタックの使いかたについては,「Interface」2006/02 特集記事の「PowerPCアセンブラのエッセンス」に詳しいので,そちらも参照してほしい.

で,スタックフレームの先頭8バイトは空けてレジスタの値を保存するように修正した.

ちなみに従来,スタックへの値保存の都合で,GPR0とGPR1の値が逆になって保存されていた.しかし今回の修正で,そのような都合を考える必要が無くなったので,ひっくり返さずにそのまま保存するように修正した.

次に,stublib.c の修正.

diff -ruN porting03/stublib.c porting05/stublib.c
--- porting03/stublib.c Tue Apr 7 23:21:40 2009
+++ porting05/stublib.c Sun Apr 12 12:09:59 2009
@@ -1,3 +1,5 @@
+#include
+
#include "kozos.h"
#include "thread.h"
#include "stublib.h"
@@ -5,13 +7,10 @@

#include "lib.h"

-#define SERIAL_NUMBER 0
+#define SERIAL_NUMBER 1

/* Number of registers. */
-#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)
+#define NUMREGS (32+2*32+7) /* GPR(32), FPR(32), SRR0/1, CR, CTR, LR, XER, FPSCR */

enum regnames {
GPR0, GPR1, GPR2, GPR3, GPR4, GPR5, GPR6, GPR7,
@@ -19,10 +18,10 @@
GPR16, GPR17, GPR18, GPR19, GPR20, GPR21, GPR22, GPR23,
GPR24, GPR25, GPR26, GPR27, GPR28, GPR29, GPR30, GPR31,

- /* FPR * 32 */
+ FPRS, /* FPR * 32 */

/* See gdb/ppc-linux-nat.c or gdb/regformats/reg-ppc.dat */
- SRR0 = (32+2*32), SRR1, CR, LR, CTR, XER
+ SRR0 = (32+2*32), SRR1, CR, LR, CTR, XER, FPSCR
};

#define SP GPR1

従来はコンソールとGDBでシリアルを共用していた(PSC1を共通で使っていた)が,操作が面倒なのでコンソールにはPSC1,GDBにはPSC2を使うように修正.これにより,コンソール専用とGDB専用で,シリアルケーブル2本で接続するようになった.あと ppc-stub.c で行ったFPSCRの追加修正をこっちにも反映.

次に,startup.s でのレジスタの保存方法の修正でGPR0とGPR1の値をひっくり返さなくてもよくなったので,その修正.

@@ -65,12 +64,8 @@

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);
+ memcpy(&registers[GPR0], &thp->context.gpr[0], sizeof(registers[0]) * 32);
+ memset(&registers[FPRS], 0, sizeof(registers[0]) * 2 * 32);

registers[PC] = thp->context.pc;
registers[MSR] = thp->context.msr;
@@ -82,9 +77,7 @@

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);
+ memcpy(&thp->context.gpr[0], &registers[GPR0], sizeof(registers[0]) * 32);

thp->context.pc = registers[PC];
thp->context.msr = registers[MSR];

次に,命令キャッシュのクリアを追加する.

GDBはブレークポイントやステップ実行の際に,命令置き換えが頻繁に行われる(これはset debug remote 1 にして,GDBとスタブのやりとりを見ているとよくわかる).しかしどうもMPC5200というCPUは,命令コードを書き換えたら命令キャッシュをクリアしないと,書き換え前の古い命令がキャッシュに残ってしまっていて,古い命令が実行されてしまうことがあるようだ.で,これはキャッシュの状態によって発生したりしなかったりするので,非常に見つけにくいバグの原因になる.

これについては「Interface」2009/04の「実践的PowerPC活用テクニック」第9回でも言及しているので,そちらも参考にしてほしい.

@@ -94,6 +87,22 @@
thp->context.xer = registers[XER];
}

+static void clear_icache_all()
+{
+ extern unsigned long _etext;
+ int addr;
+
+ asm volatile ("sync");
+ asm volatile ("isync");
+
+ for (addr = 0x0; addr < (int)&_etext; addr += CFG_CACHELINE_SIZE) {
+ asm volatile ("icbi 0,%0" :: "r"(addr));
+ }
+
+ asm volatile ("sync");
+ asm volatile ("isync");
+}
+
int stub_proc(kz_thread *thp, int signo)
{
gen_thread = thp;
@@ -103,7 +112,12 @@
clearDebugChar();
handle_exception(signo);

+ registers[MSR] &= 0xffff;
+
stub_restore_regs(gen_thread);
+
+ /* 命令書き換えが行われている場合があるので,命令キャッシュを全クリアする */
+ clear_icache_all();

return 0;
}

ついでにMSRの上半分をクリアする処理を追加.まあこれと同等のことを startup.s のdispatch 処理でも行っているし,rfiの際にはSRR1の上16ビットはMSRに反映されないようなので不要だとは思うのだけど,いちおう入れておいた.

次に thread.c について.

diff -ruN porting03/thread.c porting05/thread.c
--- porting03/thread.c Tue Apr 7 23:21:40 2009
+++ porting05/thread.c Sun Apr 12 12:09:59 2009
@@ -101,7 +101,7 @@

memset(thp->stack, 0, SIGSTKSZ);

- thp->context.gpr[0] = (int)thp->stack; /* GPR0とGPR1は逆に設定 */
+ thp->context.gpr[1] = (int)thp->stack;
thp->context.gpr[3] = (int)thp;
thp->context.gpr[4] = (int)argc;
thp->context.gpr[5] = (int)argv;
@@ -476,6 +476,7 @@
case 0x07: signo = SIGTRAP; break;
case 0x09: signo = SIGALRM; break;
case 0x0c: signo = SIGSYS; break;
+ case 0x0d: signo = SIGTRAP; break;
default:
signo = SIGBUS;
break;
@@ -483,8 +484,8 @@

/* _startup.s:_intr でGPR1を INTR_STACK_START - 160 に設定している */
p = (int *)(INTR_STACK_START - 160);
+ p += 2;
for (i = 0; i < 32; i++) {
- /* 注意:grp0,gpr1は逆に格納されている */
current->context.gpr[i] = *(p++);
}
current->context.lr = *(p++);

GPR0とGPR1をひっくり返さなくてすむようになったので,その対応.あとMSR_SEフラグが立ってステップ実行されたときに,0xd00 の割り込みが発生するので,それをラッチしてSIGTRAPとするように対応を入れた.あと割り込み時のスタックの先頭8バイトは空けるようにしたので,その修正.

最後に,強制ブレークの修正.

@@ -534,6 +535,8 @@
;
}

+void breakpoint();
+
void kz_trap()
{
asm volatile ("trap");
@@ -541,7 +544,7 @@

void kz_break()
{
- asm volatile ("trap");
+ breakpoint();
}

void kz_srvcall(kz_syscall_type_t type, kz_syscall_param_t *param)

従来はトラップ命令を直接呼んでいたのだけど,ステップ実行されたときに命令を飛ばす処理が追加されたので,スタブの breakpoint() を呼び出すように修正した.

修正は以上.では,動作を試してみよう.

今回はGDBはPSC2を使うように修正したので,シリアルケーブルは2本使い,通常のコンソール用途とGDB用で区別する.

まずはファームウエア作成し,ボードにダウンロードして起動する.

画像はこちら

ここでbreakコマンドを実行し,強制ブレークする.

> break

第3回と同様にして,emacs から gdb を起動する.

画像はこちら

正常にブレークしている.next コマンドでステップ実行してみよう.

画像はこちら

もう1回.

画像はこちら

もう1回.

画像はこちら

とくに問題なさそうだ.continue で処理続行してみる.

画像はこちら


> break
OK
>

プロンプトが出てきた.正常に continue できたみたい.

次に,ブレークポイントを張ってみよう.まずはもう一度 break でブレークする.

> break
OK
> break



画像はこちら

break コマンドで,dummy_func()にブレークポイントを張って continue する.

画像はこちら

> break
OK
> break
OK
>

再びプロンプトが出てきた.call コマンドで dummy_func() を実行してみよう.

> break
OK
> break
OK
> call



画像はこちら

おー,dummy_func() でブレークした.

ステップ実行してみよう.

画像はこちら

繰り返し実行してみたがとくに問題無し.で,continue で処理続行.

画像はこちら


> break
OK
> break
OK
> call
OK
>

再度プロンプトが出てきた.問題無しだ.

いやーデバッグたいへんだった.今回はけっこうデバッグに時間がとられてしまったのだけど,やっぱレジスタ周りとかスタック周りとかって,デバッグ難しいよね...わけわからん現象発生するし.このへんは慣れるしかないのだろうか.

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

実は今までちょ~重要なのだけどあまり考えていなかった点について,今回は考えてみよう.それは,「ライセンス」だ.

日本国の法律では,著作物は作成した時点で自動的に著作権が発生し,それを放棄することはできない(のだと思った).なので,「このソフトウエアは著作権を放棄しています」といういわゆる「パブリック・ドメイン」というものは,日本の法律ではあり得ない.

で,KOZOSはオープンソースでありフリーソフトウエアなので使いたい人が自由に使ってしまって構わない.なのだけど,KOZOSではいままではとくにライセンス条項は明記していなかった.なので,厳密に言うと「使いたくてもライセンスが明記していないので使っていいのかよくわからないので,作者である坂井に確認してからでないと使えないもの」であったわけだ.(まあ,使ってしまって構わないけどね)

で,まあ今後発展させていくことも考えると,やっぱしここらでライセンスをはっきり決めておかないといけない.ということでライセンスをどうしようか,ちょっと考えてみた.
  • GPLにする.
  • BSDライセンスにする.
  • KL-01にする.
  • 独自ライセンスにする.
まずGPLについてなのだけど,これはもともとFSFの主宰者である Richard Stallman 氏が提唱したライセンスだ.もともとぼくは昔フリーソフトウエアを何本か書いていた時代があったのだけど,最初は適当な独自ライセンスにしていたのだけど,そのうちGPLにするようになった.理由は,単にライセンスを考えるのが面倒だったのと,とりあえずGPLにしておけば,フリーソフトウエアであることが保証されるからだ.

ただ,組み込み系ではGPLはちょっと難しい点がある.というのはGPLにすると,そのソースコードを静的リンクしたすべてのソースコードについて,ソースコード公開の義務が発生する.で,これは企業では嫌がられることが多い(ノウハウが流れ出てしまったり,セキュリティホールが丸見えだったり,改造ファームを作られてしまったりするから).

これはモジュールを別にしてダイナミックローディングしたりすることで回避できたりするようなのだけど,KOZOSはOSなのでファームウエアの根幹部分にあり,そんなことは当然できない.まあKOZOSは実用OSというよりホビー用途という意味合いが強いので,これをそのまま実用OSとして組み込んで使うような企業もないとは思うけど,そーいう理由でKOZOSにGPLは望ましくない.

こういった理由で組み込みの世界では,GPLがけっこう嫌がられたり悪く言われたりすることは多いのだけど,個人的にはそういうのはどうかと思う.というのは,フリーソフトウエアの製作者というのは,その人なりのいろんな思いがあって,ソフトウエアを作っている.ライセンスというのは,その人がそのソフトウエアをどのように使ってほしいのかという,思いの結晶なのだ.

GPLにそのような制限があるのは,フリーソフトウエアがフリーソフトウエアのままであり続けられるようにという理念があるためだ.なので悪く言うのは筋違いだし,そんなに嫌なら使わずに,自分で作るべきだ.

フリーソフトウエアって,どうしても製作者の顔が見えないというか,それを作っている人がいるということを意識しないで(はじめからそのへんにあるものとして)使ってしまうひとがけっこう多いものだけど,それを作っている人は必ずいる.なので,他人がそのライセンスについて,どうこう言うのは見当違いだ.「あのソフトは糞」とか「あのソフトはGPLだから役に立たねー」なんて気軽に言っていいものではない.

たとえば,ボランティアで公園の掃除をすることを考えてほしい.掃除してくれている人に対して,「あの掃除のしかたじゃ効率悪すぎ,バカ」とか「あんな掃除のしかたじゃキレイにならねーよ」なんて言っている人がいたとしたら,それって最低だとおもうじゃあないですか.

で,ライセンスをどうするかに話を戻すと,次に考えられるのはBSDライセンスだ.これはカリフォルニア大学バークレー校がBSDの公開の際に付加したライセンスで,こちらにはソースコード公開の義務は無い.ただ,使ったら使ったということを明記しなければならないというものだ.

次にKL-01なのだけど,これはOSASKで使用されているライセンスなのだけど,ライセンス条項としてはかなり緩い.ただ特許の悪用には注意が配られていて,特許の取得は禁止されている.まあ各ライセンスについては,ぼくも法律的なことはあまり詳しくないのと,あまり無責任なことは書きたくないので,詳しくはそれぞれ調べてほしい.

最後に独自ライセンスにする,という手もある.まあでも法律的なことまで考えて書くとなるとちょっと難しい面があるので,既存のライセンスを流用するのがいいだろう.

で,KOZOSのライセンスをどうしようかいろいろ考えたのだけど,結局KL-01にすることにした.というのは,すでにライセンスを付加せずに公開してしまっているので,いまさらGPLとかにしてしまうと,すでに使っている人に対してなんか微妙だし,なによりも,自由に使ってほしいと思うからだ.

ただしデバッグスタブに関しては GDB からの流用なので,そのままのライセンス(パブリック・ドメインとして,著作権が放棄されている)とする.ただ,著作者情報くらいは入れておいたほうがいいかも.

本来はファイルのひとつひとつすべてに先頭にライセンス条項を埋め込んでおくべきなのだけど,すでに公開してしまっているソースコードを変更するのは避けたいので,とりあえずはディレクトリ内にライセンスに関する説明とKL-01の原文を置くことにする.(これは,今までの公開ぶんすべてについて,そのようにしよう)

ちなみにこれらの文章はすべて日本語だ.まあ英語で書いてあるとかっこいいのだけど,個人的には,やはり日本語で書くべきだと思う.ていうか,製作者が自身の母国語で書くべきだと思う.というのは,ライセンスというのは法律文書なので,一言一言の正確さが非常に重要だ.見ぶり手ぶりでなんとかニュアンスが伝わればいい,というものではない.なので,製作者自身がきちんと内容を理解できて,言葉のニュアンスなどもわかるネイティブな言葉で書くべきだ.

まあ日本語で書くと,ライセンスくらい英語で書けとか,英訳したものも添付しろとか言われそうだけど,そーいう意味で,個人的には日本語のみで書くべきだと思う.英語に不慣れな人が書いて,日本語版と英語版に違いがあったりしても困るからね.製作者自身もライセンス条項を正確に読めないのでは話にならんし.英語じゃないと外国の人が使いたいときに困るよとかいう人もいるかもしれないが,製作者が日本人なのだから,使いたいなら日本語を読むべきだ.逆の立場のときは,日本語訳なんて親切につけといてくれるわけではないわけだしね.
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

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

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