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

運転疲れとあと腹が減っているのだが,書きためていたぶんがあるので,前回の続きを一気に書いてしまおう.

前回はプロファイラの -pg の機能を利用して,ログ保存をしてみた.今回はもうちょっと発展させてみる.

前回説明したのだけど,_mcount()から戻る際にはLRの値を設定した上で戻る必要がある.で,前回の実装では,_mcount() 側でLRを設定し,実際のリターンは bctr で行っていた.まあこれはこれでいいのだけど,実はここでblrlで戻ってしまえば,戻るときにLRの値が書き変わることで本来の関数の戻り先が _mcount() 側になるので,関数の先頭と終端で _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_begin

lwz 10,8(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)
blrl

stw 3,16(1)
stw 4,20(1)
lwz 3,8(1)
bl logging_end

lwz 10,52(1)
mtlr 10
lwz 3,16(1)
lwz 4,20(1)
addi 1,1,48
blr

関数呼び出し時には logging_begin() が呼ばれ,関数の終了時(return文によりblr命令が実行されたとき)にはblrlの直後に戻り,logging_end() が呼ばれるようになっている.

logging_end() の呼び出し時に保存が必要なレジスタなのだけど,PowerPCのABIでは関数の戻り値はGPR3,GPR4で返すということになっている.なのでこの2つだけ保存しておくようにしている(でないと呼び出し元の関数の戻り値が破壊されてしまうことになる).

logging.c は,logging() を logging_begin() に名前変更して,あと logging_end() を追加した.以下のようなかんじ.

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

void logging_end(int addr)
{
if (!logging_disable) {
logging_buf[logging_cur++] = addr | (1<<31);
if (logging_cur >= LOGGING_BUF_SIZE)
logging_cur = 0;
}
}

logging_end()では,とりあえずアドレスの最上位ビットを立てて保存することで,logging_begin()によるログと区別できるようにしてある.

ソースコードは以下のような感じ.

では実行してみよう.前回と同じように起動して,logコマンドを実行してみる.

> echo aaa
aaa
OK
> log
00043508 000430e4 800430e4 0004314c 8004314c 000430e4 800430e4 80043508
00043268 80043268 00043508 000430e4 800430e4 0004314c 8004314c 000430e4
800430e4 80043508 00043268 80043268 00043268 80043268 00043268 80043268
00043508 000430e4 800430e4 0004314c 8004314c 000430e4 800430e4 80043508
00043268 80043268 00043508 000430e4 800430e4 0004314c 8004314c 000430e4
800430e4 80043508 00043268 80043268 00043508 000430e4 800430e4 0004314c
8004314c 000430e4 800430e4 80043508 00043268 80043268 00043508 000430e4
800430e4 0004314c 8004314c 000430e4 800430e4 80043508 00043268 80043268
OK
>



おー,0x00043xxx と 0x80043xxx が表示されている.0x00043xxx が関数呼び出し,0x80043xxx が関数の終了に相当する.いいかんじだ.

ところでこれには現状で問題がある.というのは,引数の数が8個を越えるとGPR3~GPR10では渡しきれないので,残りの引数はスタックに積んで渡すことになる.しかし _mcount() 内部では呼び出し元の関数の前に,自前でスタックを積んでしまっているので,呼び出し元の関数は,引数が保存されているスタックを正常に読み取ることができない.なので9番目以降の引数は,本来の値とは異なるおかしな値が渡されることになる.(前回は _mcount() 側のスタックを解放してから呼び出し元に戻っているので,このような問題は無い)

この問題の根本的な原因は,_mcount()側でスタックを確保しっぱなしになっているということだ.これを回避する手段については,前回のように _mcount() から戻る際にはスタックを全解放するしかないが,それをやると2回目の呼び出し時にLRの値が残っていないので,正常に戻れない.

対策としては,以下が考えられる.
  1. 引数は8個までと制限を設ける.
  2. 関数終了時にも _mcount() を呼ばせるのはあきらめて,前回のコードを使う.
  3. _mcount()内でスタック作成時に,ひとつ前のスタックフレームの数バイトをコピーして持っておく(結局のところ引数の数に制限はあるので根本的な解決ではないが,制限を拡張してチューニングすることができる)
  4. LRの値を保存するための独自スタックを別に作る.
きちんとやろうとしたら4の方法しか思いつかないのだけど,これはスタックの計算処理が入るので,mcount()がちょっと複雑になってしまう.まあ現実的なところでは1か2だろうか.

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

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

まずはプロファイラの説明だけど,プロファイラって積極的に使っているひとはなにげにあまりいなかったりするものだけど,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()の先でうかつな処理をすると,動作がすげー重くなってしまう可能性はある.

今回はログ保存を実装したけど,他にもアイディア次第でこれってけっこういいアイディアだと思うのだけどどうでしょう.
AD

KOZOSのコアサイズ

テーマ:
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でなんとかそのへんのイメージを払拭したいものだ.

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

前回までで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 *)®isters[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(®isters[GPR2], &thp->context.gpr[2], 4*30);
+ memcpy(®isters[GPR0], &thp->context.gpr[0], sizeof(registers[0]) * 32);
+ memset(®isters[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], ®isters[GPR2], 4*30);
+ memcpy(&thp->context.gpr[0], ®isters[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の原文を置くことにする.(これは,今までの公開ぶんすべてについて,そのようにしよう)

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

まあ日本語で書くと,ライセンスくらい英語で書けとか,英訳したものも添付しろとか言われそうだけど,そーいう意味で,個人的には日本語のみで書くべきだと思う.英語に不慣れな人が書いて,日本語版と英語版に違いがあったりしても困るからね.製作者自身もライセンス条項を正確に読めないのでは話にならんし.英語じゃないと外国の人が使いたいときに困るよとかいう人もいるかもしれないが,製作者が日本人なのだから,使いたいなら日本語を読むべきだ.逆の立場のときは,日本語訳なんて親切につけといてくれるわけではないわけだしね.