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

ここまで来て,ブレークポイントを試していないことに気がついた.デバッガを利用していてブレークポイントは非常に心強い機能で,ブレークポイントとステップ実行さえできればいいという場合も多いのだが,今回はブレークポイントの実現方法について説明しよう.

そもそもブレークポイントがどのように実現されるかというと,2通りある.まずひとつ目だが,CPUによっては,特定のアドレスの命令を実行しようとすると割り込みがかかるように設定できるものがある.まあそーいう専用のレジスタを持っているわけだが,これを利用してブレークポイントを実現するものをハードウエア・ブレークポイントと呼ぶ.

もうひとつは,ブレークさせたい場所の命令をトラップ命令などに書き換えておき,実行がその場所にさしかかったらトラップ命令が実行されて割り込みが上がるようにする,というものだ.これはソフトウエア・ブレークポイントと呼ぶ.

ハードウエア・ブレークポイントは,テキスト領域の書き換えを行う必要が無いので,メモリ保護で書き込み不可になっている場合でも利用できるという利点がある.ただしレジスタの数は有限なので,設定数に限りがある(そしてその上限はわりと少ない).

これに対してソフトウエア・ブレークポイントは,テキスト領域の書き換えを行うため,メモリ保護で書き込み不可になっていると利用できない.これは場合によってはわりと重要な問題で,KOZOSではこの問題を解消するためにt2w というツールを使ってテキスト領域を書き込み可としてマッピングしている(第11回参照).また,制御がけっこうめんどい.制御については後で説明するが,実際に見てもらうと,うわーデバッガってこんなめんどくせーことやってるんだーと思うことだろう.まあとは言ってもそーいう制御はgdbのお仕事なので,あまり気にする必要は無い.

ただしソフトウエア・ブレークポイントだと,ブレークポイントの個数に制限が無いという利点はある.ブレークポイントをかけまくる機会は結構多く,これは実のところ,けっこうなアドバンテージだ.

で,gdbではハードウエアなのかソフトウエアなのかどちらなのかというと,通常の break コマンドによるブレークポイントには,ソフトウエア・ブレークポイントが利用される.ハードウエア・ブレークポイントの設定には,break ではないなんか別のコマンド(忘れた)が利用されるようだ.

さて,ブレークポイントの実現方法についてなのだけど,こーいうのを調べたい場合には,リモートデバッグの場合には,GDBとスタブの間の通信を覗き見るのが一番手っ取り早い.ということで実際に見てみよう.

今回はソースコードの修正はちょろっとだけ.

修正は telnetd.c に対するものだけだ.

diff -ruN kozos15/telnetd.c kozos16/telnetd.c
--- kozos15/telnetd.c Mon Nov 12 23:01:07 2007
+++ kozos16/telnetd.c Mon Nov 12 23:09:28 2007
@@ -15,6 +15,16 @@
int telnetd_id;
int telnetd_dummy;

+static int dummy_func(int n)
+{
+ int i, sum;
+ sum = 0;
+ for (i = 1; i < n; i++) {
+ sum += i;
+ }
+ return sum;
+}
+
static int command_main(int s, char *argv[])
{
char *p;
@@ -42,6 +52,8 @@

if (!strncmp(buffer, "echo", 4)) {
write(s, buffer + 4, strlen(buffer + 4));
+ } else if (!strncmp(buffer, "call", 4)) {
+ dummy_func(10);
} else if (!strncmp(buffer, "down", 4)) {
int *nullp = NULL;
*nullp = 1;

ブレークポイントを張るテスト用に dummy_func() を追加し,call コマンドを実行すると dummy_func() が呼ばれるようにした.

まずは実行形式 koz を起動して,前回同様 gdb で接続し continue して動作開始し,telnet で接続するところまでやってみよう.

Mon Nov 12 23:31:23 2007
Mon Nov 12 23:31:24 2007
Mon Nov 12 23:31:26 2007
Mon Nov 12 23:31:27 2007
Mon Nov 12 23:31:28 2007
...

上は,動作開始したところ.

% telnet 192.168.0.3 20001
Trying 192.168.0.3...
Connected to 192.168.0.3.
Escape character is '^]'.
>

telnet による接続もOK.

ここでgdbでCtrl-C(emacsからの利用の場合には,Ctrl+Cを2回押す)でブレークする.

画像はこちら

このとき,gdbとスタブの間では以下の通信が行われている.

Mon Nov 12 23:32:06 2007
Mon Nov 12 23:32:07 2007
Mon Nov 12 23:32:08 2007
($T054:58e60e08;5:58e60e08;8:c5a80408;#c0)[+]
[$M8048088,1:55#c2](+)($OK#9a)[+]
(この状態で停止)

時刻表示が行われていたが停止し,gdbコマンドが送受信されている.

まず[]でくくられているのはgdbからスタブへの通信,()でくくられているのはスタブからgdbへの通信だ.で,gdbコマンドの読み方だが,

$<コマンド><パラメータ>#<チェックサム>

という形式になっている.相手からのコマンドを正常に受信した場合にはACKとして'+'を,異常受信の場合には'-'を返すというプロトコルだ.で,上の通信の内容だが,

($T054:58e60e08;5:58e60e08;8:c5a80408;#c0)[+]

まず Ctrl-C により stubd から kz_break() が呼ばれることでブレークし,スタブがTコマンドを送信している.Tコマンドはスタブ側での割り込み発生時にスタブからgdbに発行されるコマンドで,まあパラメータが連続していてわかりにくいのだが,上の通信の内容をパラメータごとに分解すると以下のようになる.
  1. T05
  2. 4:58e60e08;
  3. 5:58e60e08;
  4. 8:c5a80408;
  5. #c0
  6. +
まずひとつめの「T05」だが,「T」は割り込みの発生を表わす.で,割り込みの内容は 0x05,つまり SIGTRAP である.(割り込みの内容はシグナル番号で表現される.シグナル番号については man signal 参照)

さらに割り込み発生時には,一部の代表的なレジスタの内容を送信する.これはgdbから改めてレジスタの値を取りにいくと通信量が増えるのと,割り込み発生しているのでどうせレジスタ情報が必要になるために,あらかじめ送ってしまうということのようだ.多分.

で,レジスタ情報の送りかたなのだが,「4:58e60e08;」のように,レジスタ番号とその値というフォーマットで送信する.レジスタ番号は,i386-stub.c で定義されている

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};

の番号になる.つまり「4:58e60e08;」というのは,上記 regnames の4番目(enumはゼロから数えることに注意)であるESP(スタックポインタ)の値が0x080ee658 であることを表わす(リトルエンディアンになっていることに注意).同様に,「5:58e60e08;」はEBPが0x080ee658,「8:c5a80408;」はPC(プログラムカウンタ.i386ではEIPともいう)が0x0804a8c5であることを表わす.だいたいどのアーキテクチャでも,プログラムカウンタとスタックポインタくらいの値は送信するようだ.

最後に「#c0」はチェックサムの値となる.正確にいうと「#」がコマンドの終りを表わし,その後に 0xc0 という値のチェックサムが付加されている.さらにgdbから「+」が返信され,コマンドが無事に受信されたことが通知されている.

スタブ側からTコマンドによるシグナル発生を受信したgdbは,次に

$M8048088,1:55#c2

というコマンドを送信している.(で,スタブ側が「+」を返している)Mコマンドは特定アドレスへの書き込みを意味する.上のコマンドは,

「0x08048088 に1バイトの値として0x55を書き込め」

というgdbからの指示だ.

ここでまず注意したいのは,アドレス値が今度はビッグエンディアンになっているということだ.このようにエンディアンは値によって(そして,ターゲットのプラットホームによって)変わってくるので,注意する必要がある.

さらに,0x08048088 というアドレスだ.これは readelf で実行形式 koz を解析すると

% readelf -a koz
...
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .init PROGBITS 08048074 000074 000011 00 AX 0 0 4
[ 2] .text PROGBITS 08048088 000088 05efb6 00 AX 0 0 4
...

となっており,テキスト領域の先頭であることがわかる.テキスト領域のオフセットは 0x000088 となっているが,実際の実行形式の16進ダンプを見てみると

% hd koz | head
00000000 7f 45 4c 46 01 01 01 09 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 03 00 01 00 00 00 88 80 04 08 34 00 00 00 |............4...|
00000020 e0 d2 07 00 00 00 00 00 34 00 20 00 02 00 28 00 |......4. ...(.|
00000030 16 00 13 00 01 00 00 00 00 00 00 00 00 80 04 08 |................|
00000040 00 80 04 08 3a 7b 06 00 3a 7b 06 00 07 00 00 00 |....:{..:{......|
00000050 00 10 00 00 01 00 00 00 00 80 06 00 00 00 0b 08 |................|
00000060 00 00 0b 08 1c 20 00 00 4c 8c 01 00 06 00 00 00 |..... ..L.......|
00000070 00 10 00 00 83 ec 0c e8 ec 00 00 00 e8 13 ef 05 |................|
00000080 00 83 c4 0c c3 00 00 00 55 89 e5 57 56 53 83 ec |....U..WVS..|
00000090 0c 83 e4 f0 8b 5d 04 89 d7 8d 74 9d 0c 85 db 89 |.....]..t...|
%

となっている.テキスト領域の先頭はオフセット0x000088の位置であり,その位置の値は0x55になっている.つまり「$M8048088,1:55#c2」は,0x08048088 にすでに 0x55 という値があるにもかかわらず,0x55を上書きするということなので,意味の無い操作に見える.

で,gdbがいったいこれで何がしたいのかというと,おそらくテキスト領域への書き込みが行えるかどうかのチェックをしているのだと思う.t2wでテキスト領域の書き込みを可にしてあるので書き込みは成功し,スタブは「OK」という文字列を返している.これによりgdbはテキスト領域に書き込み可能であることを知ることができるわけだ.

で,gdb から break コマンドで,dummy_func() にブレークポイントを張ってみよう.

画像はこちら

[$m804aa78,1#97](+)($55#6a)[+]
[$m804aa78,1#97](+)($55#6a)[+]
[$m804aa78,1#97](+)($55#6a)[+]
[$m804aa78,1#97](+)($55#6a)[+]
[$m804aa79,1#98](+)($89#71)[+]
[$m804aa7a,1#c0](+)($e5#9a)[+]
[$m804aa7b,1#c1](+)($83#6b)[+]
[$m804aa7c,1#c2](+)($ec#c8)[+]
[$m804aa7d,1#c3](+)($08#68)[+]
[$m804aa7e,1#c4](+)($c7#9a)[+]
[$m804aa7e,1#c4](+)($c7#9a)[+]
[$m804aa7e,1#c4](+)($c7#9a)[+]
(この状態で停止)



mコマンドは特定アドレスの読み込みを意味する.このへんのコマンドの意味は,移植元である i386-stub.c を読めばわかるので,必要ならばよく読んでほしい.フォーマットはMコマンドとほぼ同じで,たとえば「$m804aa78,1」ならば,「0x0804aa78というアドレスから1バイトの値を読め」という意味になる.で,応答としてスタブが「$55」という値を返している.つまり0x0804aa78の値は0x55,ということだ.

で,0x0804aa78~0x0804aa7eまでの値を読み出しているのだが,これはいったい何をやっているのだろうか?

まず0x0804aa78なのだが,readelfの結果では

106: 0804aa78 48 FUNC LOCAL DEFAULT 2 dummy_func

となっており,関数dummy_func()があることがわかる.つまりブレークポイントの設定時には,dummy_func()の先頭位置の実行コードを数バイト,gdbが読み出しているようだ.

で,これが何をやっているのかなのだけど,実は関数にブレークポイントを設定した場合には,その関数のほんとうに一番トップの位置にトラップ命令を仕掛けるわけではなくて,スタックの確保とかレジスタのスタック退避を行った後の位置にトラップ命令を仕掛けるようだ.でないと,関数の先頭でのブレーク時にはスタックが未設定となってしまい,ローカル変数の値などが壊れて見えてしまうからだろう.おそらく.

なので,関数の先頭部分の命令を読み出し,スタック確保などが行われた後のブレークポイント設定に適切な位置を調べているわけだ.

ソフトウエアブレークポイントとしてのトラップ命令の設定が行われていないが,実は break コマンドを実行しただけではトラップ命令は設定されない.continue を実行すると,そこで初めてトラップ命令が設定されることになる.

で,continue で動作再開する.

画像はこちら

[$m8048088,1#3e](+)($55#6a)[+]
[$M8048088,1:cc#1e](+)($OK#9a)[+]
[$m804aa7e,1#c4](+)($c7#9a)[+]
[$M804aa7e,1:cc#a4](+)($OK#9a)[+]
[$c#63](+)Mon Nov 12 23:33:51 2007
Mon Nov 12 23:33:52 2007
Mon Nov 12 23:33:54 2007
Mon Nov 12 23:33:55 2007
Mon Nov 12 23:33:56 2007
Mon Nov 12 23:33:57 2007
...

時刻表示が動作再開していることに注目.

continue 実行時には,以下のことを行っているようだ.
  1. まず「m8048088,1」により,0x08048088 (これはテキスト領域の先頭のアドレス)の値を読み出す → 値として 0x55 が返っている.
  2. 「M8048088,1:cc」により,テキスト領域の先頭にトラップ命令として0xcc を設定している.
  3. さらに「$m804aa7e,1」により,関数 dummy_func() の先頭付近の値を読み出している → 値として 0xc7 が返っている.
  4. さらに「$M804aa7e,1:cc」により,関数 dummy_func() の先頭付近にトラップ命令(0xcc)を設定している.
  5. 最後に,「$c」を送信して continue を指示している.
これを見る限り,0xcc と言うのが i386 のトラップ命令のようだ(未確認だけど).つまり0xccを実行すると,CPUにトラップ割り込みが上がることになる.

dummy_func() の先頭付近にトラップ命令を仕掛けるのはわかるのだが,気になるのはテキスト領域の先頭(readelf の結果を見ると Entry point address もやはり 0x8048088 となっているので,実行が開始される位置)にもトラップ命令が仕掛けられているということだ.うーんなぜだろう,不明.

まあこれでトラップ命令が仕掛けられ,実行形式 koz の実行が続行(continue)されることになる.ちなみにトラップ命令設定の前に,mコマンドにより設定場所の値を読み出しているのは,あとでトラップ命令を削除する際に元の値に戻す必要があるから.

telnetから call コマンドを実行して,dummy_func()を呼び出してみよう.

% telnet 192.168.0.3 20001
Trying 192.168.0.3...
Connected to 192.168.0.3.
Escape character is '^]'.
> call
(この状態で停止)



画像はこちら

おー,ブレークした.

このときスタブは以下のような通信を行っている.

Mon Nov 12 23:34:36 2007
Mon Nov 12 23:34:37 2007
Mon Nov 12 23:34:38 2007
Mon Nov 12 23:34:39 2007
Mon Nov 12 23:34:40 2007
($T054:ec461208;5:f4461208;8:7faa0408;#b0)[+]
[$g#67](+)($0000000097730a080000000060300c08ec461208f4461208ec471208000000007faa040812020000330000003b0000003b0000003b0000003b0000001b000000#9e)[+]
[$P8=7eaa0408#ef](+)($OK#9a)[+]
[$M8048088,1:55#c2](+)($OK#9a)[+]
[$M804aa7e,1:c7#78](+)($OK#9a)[+]
[$m81246fc,4#9b](+)($0a000000#b1)[+]
(この状態で停止)

まず「$T054:ec461208;5:f4461208;8:7faa0408;」というコマンドがスタブからgdbに送信されているが,これは先に説明した通り,シグナルの発生をgdbに通知している.「T05」なので,やはり SIGTRAP が発生している.レジスタ情報としてはこれも先と同様に ESP,EBP,PC の値を送信している.これから,トラップ命令によりブレークした場所が特定できる.

PCの値は「8:7faa0408;」となっており,0x0804aa7f であることがわかる.ブレークポイントの設定時には,0x0804aa7e にトラップ命令を埋め込んだので,シグナル発生時には,PCは次の命令を指していることがわかる.

で,次にgdbから「$g」というコマンドが送信されている.gコマンドというのは,レジスタの取得だ.つまりトラップが発生したので,「その時の状態を知るためにレジスタ情報をよこせ」とgdbから指令が来ているわけだ.

スタブからは

$0000000097730a080000000060300c08
ec461208f4461208ec47120800000000
7faa040812020000330000003b000000
3b0000003b0000003b0000001b000000

という長~~~い返信が返されている.これがレジスタ値の一覧なのだが,各種レジスタの値が,上で説明した regnames で表わされる順番に(区切り無しで,一気に)送信されている.パット見でこれもリトルエンディアンになっているので注意.ちなみに簡単に説明すると,regnames の定義では「EAX, ECX, EDX, EBX, ...」という順番になっているので,上の返信結果では

EAX = 0x00000000
ECX = 0x080a7397
EDX = 0x00000000
EBX = 0x080c3060
...

という内容になっている.

次にgdbから「$P8=7eaa0408」というコマンドが送信されている.Pコマンドというのはレジスタの値設定で,この場合はレジスタ番号が8番目(くどいようだが,レジスタ番号は regnames での定義順番である)であるPC(プログラムカウンタ)に,0x0804aa7e という値を設定している.PCの値は 0x0804aa7f となっていたので,ひとつ前の命令に戻っているようだ.これはなぜかというと,ひとつ前の命令は実はトラップ命令(0xcc)に置き換えてしまっているため,実行されていない.なのでこの後の動作再開を見越して,PCをひとつ手前に戻して,さらに「$M8048088,1:55」「$M804aa7e,1:c7」によってトラップ命令を削除して元の命令コードに戻しておく,ということをしているようだ.

さらに「$m81246fc,4」により,0x081246fc から4バイトの値を読んでいるが,うーんこれはちょっと不明.なにをやっているのだろう.ちなみに値としては0x0000000aというものが返っていて,これは10進数だと「10」になるが,telnetd.c 中での call コマンドでのdummy_func() 呼び出し時の引数がやはり10になっているので,このへんに関係があるのかもしれない.

continue で動作続行してみよう.

画像はこちら

[$Hc0#db](+)($#00)[+]
[$s#73](+)($T054:ec461208;5:f4461208;8:85aa0408;#80)[+]
[$m8048088,1#3e](+)($55#6a)[+]
[$M8048088,1:cc#1e](+)($OK#9a)[+]
[$m804aa7e,1#c4](+)($c7#9a)[+]
[$M804aa7e,1:cc#a4](+)($OK#9a)[+]
[$Hc0#db](+)($#00)[+]
[$c#63](+)Mon Nov 12 23:35:44 2007
Mon Nov 12 23:35:45 2007
Mon Nov 12 23:35:46 2007
Mon Nov 12 23:35:48 2007
Mon Nov 12 23:35:49 2007
Mon Nov 12 23:35:50 2007
...

時刻表示が再開されている.無事に continue できているようだ.

まず「$Hc0」というコマンドがgdbからスタブに送信されている.しかしスタブ側では「$#00」という空の応答(よってチェックサムは0x00)が返されている.これは「そのコマンドはこのスタブでは実装されていません」という意味だ.なので,gdb側では別のもっと簡単なコマンドを使う,ということになる.「$Hc0」というコマンドがどーいうものなのかはgdbのソースをよく読んでみないとわからんが,まあ実装されていなくても問題なさそうなのでいいとする.

次に「$s」というコマンドが送信されている.これはステップ実行のフラグをCPUに立てて動作続行しろ,という意味だ.たいていのCPUはデバッグ用にステップ実行フラグというのを持っていて,これを立てておくと,1命令の実行のたびにトラップ割り込みが発生することになる.で,これによってどーいうことが起きるのかというと,動作続行後に1命令を実行した直後,再度スタブに処理が渡ってくることになる.実際に,「$s」の送信の直後に「$T054:ec461208;5:f4461208;8:85aa0408;」がスタブからgdbに送信され,トラップ割り込みが発生していることがわかる.割り込みの発生場所は「8:85aa0408;」によれば0x0804aa85となる.おそらく(i386は命令が可変長なのでよくわからんが)ブレークポイントの次の命令なのだろう.

で,「$m8048088,1」「$M8048088,1:cc」「$m804aa7e,1」「$M804aa7e,1:cc」により,ブレークポイントの設定を再度行っている.ここまでの操作により,ブレークポイント上に設定したトラップ命令は取り払われ,元に戻ってしまっている.このまま動作再開すると再度ブレークポイントで停止することができないので,ステップ実行によりちょっと処理を進めて,ブレークポイントの位置を通り過ぎた後でブレークポイントを再設定する,ということを行っているわけだ(通り過ぎた後でないと,実行再開の直後にまたブレークポイントに引っかかってしまうから).

で,ブレークポイントを設定した後で,最後に「$c」により動作再開している.これでよーやく実行形式が動作再開することになる.

今回はgdbとスタブとのやりとりの内容を説明したが,どうだろうか?まあ実際のところ gdb はけっこういろいろな指示を出していて,なんでこんなことしてるんだろというものも,よく考えてみるとそりゃそうだと思える場合も多い.
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

今回はブレーク周りをちょっと整備しよう.

前回,telnet から break コマンドを実行したときに,ブレーク位置のソースコードが表示されていなかった.これは前回だか前々回だかに説明したが,telnet で break 実行時にはkz_break() が呼ばれるが,kz_break() 内部では kill() で SIGTRAP を発行しており,kill()はシステムコールのためアセンブラでさらに -g でコンパイルされていないため,gdbがソースコード情報を探すことができないからだ.

実はブレーク用にはスタブ側で breakpoint() という関数が用意されている.i386-stub.c を見ると

#define BREAKPOINT() asm(" int $3");

void
breakpoint (void)
{
if (initialized)
BREAKPOINT ();
}

のようになっている.本来は kz_break() ではこれを呼び出せばいいのだけれど,第10回で説明したように,i386-stub.c の breakpoint() を呼び出してもうまくブレークできないので,kz_break() は SIGTRAP を発行するような実装にしている.しかしこれも第10回で説明しているが,インラインアセンブラを無効化するためにi386-stub.c の先頭で

#define asm(x)

を行っているため,BREAKPOINT()が空定義になってしまっているのがまずいというか間抜けな原因となっている.なのでこのへんを直して,kz_break() で breakpoint()を呼ぶような実装に修正する.これでブレーク時にソースコードが表示されるはずだ.

で,修正したソースコードは以下のようになる.

まず i386-stub.c の修正なのだが,

+#undef asm
void
breakpoint (void)
{
if (initialized)
BREAKPOINT ();
}

のようにして,breakpoint() の直前で asm() の空定義を無効にする.これで,直後の BREAKPOINT() の行がトラップ命令を発行するようなアセンブラに置き換わるようになる.

次に,thread.c 内部の kz_break() の修正.

-void kz_break()
+void kz_trap()
{
- /*
- * スタブ付属の breakpoint() でうまくブレークできないので,
- * トラップシグナルを上げてブレークする.
- */
kill(getpid(), SIGTRAP);
+}
+
+void kz_break()
+{
+ void breakpoint();
+ breakpoint();
}

以前の SIGTRAP 発行処理は kz_trap() に改名している.kz_break() は breakpoint() を呼び出すように修正しているので,kz_break() 呼び出し時にはブレーク位置のソースコードが表示されることが期待できる.

あと stubd.c について.

diff -ruN -U 10 kozos14/stubd.c kozos15/stubd.c
--- kozos14/stubd.c Mon Nov 12 19:16:51 2007
+++ kozos15/stubd.c Mon Nov 12 19:19:21 2007
@@ -60,44 +60,31 @@
s = accept(sockt, (struct sockaddr *)&address, &len);

kz_debug(s);

/*
* スタブ付属の breakpoint() でうまく停止できないので,
* kz_break() によって停止する.
*/
kz_break();

-#if 1 /* Ctrl-C対応 */
kz_send(extintr_id, s, NULL);
- /* write(s, "+", 1); */
- /* write(s, "$O7465#25", 9); */
- /* write(s, "$X0b#ea", 7); */
- /* write(s, "$W00#b7", 7); */
while (1) {
int size, i;
char *p;
size = kz_recv(NULL, &p);
for (i = 0; i < size; i++) {
if (p[i] == 0x03) { /* Ctrl-C */
kz_break();
break;
} else {
fprintf(stderr, "\'%c\'", p[i]);
}
}
-#if 0
- if (strncmp(p, "$Hc-1#", 6)) {
- write(s, "+$#00", 5);
- }
-#endif
kz_memfree(p);
}
-#else
- kz_sleep();
-#endif

close(s);
close(sockt);

return 0;
}

こちらはまあ不要なコメントを外してすっきりさせただけなのだが,Ctrl-C 受信時の動作についてちょっと説明.

gdb利用時には,実機が動作していて gdb がブレーク待ちになっている場合には,Ctrl-C を送信することで強制ブレークして gdb に処理を渡す(=gdbのプロンプトが表示されて指示待ちになる)ことができる.このためには,gdbからの入力で Ctrl-C が送信されてきたかどうかを監視し,Ctrl-C が来た場合には強制的にブレークするような処理が必要になる.このために stubd でスタブ用ソケットを extintr に送ることでソケット監視し,ソケットが読み取り可になった(=gdbからなんらかのデータが送られてきた)場合にはCtrl-Cかどうかを判断し,Ctrl-Cならば kz_break() により強制ブレークするようになっている.

次に telnetd.c をちょっと修正.

if (!strncmp(buffer, "echo", 4)) {
write(s, buffer + 4, strlen(buffer + 4));
} else if (!strncmp(buffer, "down", 4)) {
int *nullp = NULL;
*nullp = 1;
+ } else if (!strncmp(buffer, "trap", 4)) {
+ kz_trap();
} else if (!strncmp(buffer, "break", 5)) {
kz_break();
} else if (!strncmp(buffer, "date", 4)) {

trap コマンドが入力された場合には,kz_trap() が呼ばれる処理を追加してある.kz_trap() は以前までの kz_break() が改名したもので,SIGTRAP 発行により強制ブレークされることになる.

では,実際に動かしてみよう.まず起動して gdb で接続するところまでは前回と同じ.

画像はこちら

注意してほしいのは,前回までは gdb での接続時にはソースコードは表示されていなかったが,今回はブレーク位置のソースコードが表示されているという点だ.これは stubd での接続受け付け時の kz_break() が,SIGTRAP でなく breakpoint() の呼出しに変更されたため,gdbがソースコードを検索することができるからだ.

continue で動作再開すると,各スレッドが動作を開始して,時刻表示が行われるようになる.

Mon Nov 12 19:52:20 2007
Mon Nov 12 19:52:21 2007
Mon Nov 12 19:52:22 2007
Mon Nov 12 19:52:23 2007
Mon Nov 12 19:52:24 2007
Mon Nov 12 19:52:25 2007
Mon Nov 12 19:52:26 2007
...

次に,いつも通り telnet で接続する.

% telnet 192.168.0.3 20001
Trying 192.168.0.3...
Connected to 192.168.0.3.
Escape character is '^]'.
>

trap コマンドを実行して,kz_trap() による強制ブレークを行ってみよう.

> trap
(この状態で停止)



画像はこちら

ソースコード表示がさっきと変わっていないことに注意してほしい.kz_trap() は kill() による SIGTRAP の発行であるため,くどいようだがgdbがデバッグ情報を得られずにソースコードを表示できないため,ソースコード表示はそのままになっているのだ.

upを実行して,現在のブレーク位置を確認してみよう.

画像はこちら

kz_trap()のkill()している位置でブレークしていることがわかる.

continue して動作続行し,今度は break で強制ブレークしてみる.

> break
(この状態で停止)



画像はこちら

今度は breakpoint() 内部の,BREAKPOINT() の呼び出し位置にソースコード表示が切り替わっている.ちゃんとブレーク位置が表示できているわけだ.

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

画像はこちら

さらに next でステップ実行.

画像はこちら

kz_break() の呼び出しを抜けて,ステップ実行できている.

continue で動作続行できる.

画像はこちら

Mon Nov 12 19:57:20 2007
Mon Nov 12 19:57:21 2007
Mon Nov 12 19:57:22 2007
Mon Nov 12 19:57:23 2007
Mon Nov 12 19:57:24 2007
Mon Nov 12 19:57:25 2007
...



時刻表示も正常に復活している.

最後に Ctrl-C によるブレークを試してみよう.gdbを直接起動しているならば Ctrl+C を押せばいいのだが,emacs から利用している場合には,Ctrl+C を連続で2回押すことで,ターゲットに Ctrl-C が送信される.これは,gdbが実機のブレーク待ち状態(continue 実行後など,gdbプロンプトが表示されていない状態)で行う必要がある.

画像はこちら

強制ブレークされ,ソースコードが表示された.breakpoint() の内部で停止している.

upでどこで停止しているのか確認しよう.

画像はこちら

上はupを2回実行したところ.stubd.c 内部の kz_break() 呼び出し位置でブレークしていることがわかる.stubd で Ctrl-C を受信して kz_break() を呼び出すことで,ブレークしているわけだ.

continue で,やはり動作続行できる.

画像はこちら

Mon Nov 12 20:00:11 2007
Mon Nov 12 20:00:12 2007
Mon Nov 12 20:00:13 2007
Mon Nov 12 20:00:14 2007
Mon Nov 12 20:00:15 2007
...



時刻表示が再開される.

gdbでのデバッグについてあまり詳しく説明していなかったけど,このように,実機(この場合は実行形式koz)とgdbは,同期して交互に動作する.実機が停止したらgdbに処理が渡りプロンプトが表示されてコマンド入力待ちになる.この間,実機はgdbからのコマンド待ちで,動作を停止している(KOZOSの場合には,スタブの内部でスタブ用ソケットの read() でブロックしている).で,continue とかがgdbから送信されると,実機は動作を開始し,今度はgdbが実機のブレーク待ちになる(この間,gdbはプロンプトを表示せず,コマンド入力を受け付けない).

これはgdbに処理が渡った際に,gdbがコマンド発行して状態取得している最中に実機が動作してしまうと,取得したデータの整合性が取れなくなってしまう可能性があるからだ.たとえばgdbでリンクリスト構造を追いかけている最中にリンクリストの構成が変わってしまったら,果たして何が起きるかわからない.ということで,基本として gdb による操作中は,実機は完全停止(内部ではスタブが動作するだけ.スタブは他箇所とは完全に独立して実装されるべき(KOZOSではそうはなっていないけど)なので,他箇所に影響を及ぼすことは無い)ということになる.

つまり実機とgdbは交互に同期して動作するわけなのだが,しかしこれだけだと,実機が動きっぱなしでブレークしない場合には,gdb側からなにもできないことになる(ブレークさせるためにブレークポイントを仕掛けることすらできない).これでは困るので,Ctrl+C 押下によって強制ブレークさせ,gdbが処理を奪うことができるようになっているのだ.
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

今日はなんかノッているので2本目.

前回はコンテキスト管理を getcontext()/setcontext() の枠組に置き換えた.で,従来はカーネル用コンテキストは intr_env という変数に保存していて,割り込み発生時には intr_env に setcontext() することで,カーネル用コンテキストに切替えていた.これは前回も書いたように,ユーザスレッドのコンテキストでカーネルの(システムコールなどの)処理を行うのはまずいというのが理由なのだが,実はシグナルハンドラの先頭からsetcontext() されるまでの間はやはりユーザスレッドのコンテキストが利用されてしまうので,根本的な解決になっていない.しかしシグナル発生時にスタックが利用されてしまうのは避けられず,これはある意味どーしようもない.

しかし,前にも書いたように,シグナルに関しては現在は signal() よりもsigaction() を使うのが現代的(第12回参照)なのだけど,このあたりをいろいろ調べていたら,実は sigaltstack() という,またこれがまさにこーいうときのために使うためのシステムコールがあるんだな.うーん,ほんとにラッキーな,というか,まあ誰でも考えることは同じということだろうか.

なので,sigaltstack() を使うように変更すれば,intr_env による面倒なカーネル用コンテキストへの切替え処理は不要になるんだな.で,そーいうふうに書き直してみた.

以下に修正内容を説明しよう.まずメインの修正となるのは thread.c だ.

diff -ruN kozos13/thread.c kozos14/thread.c
--- kozos13/thread.c Sun Nov 11 23:17:30 2007
+++ kozos14/thread.c Mon Nov 12 00:21:49 2007
@@ -21,14 +21,13 @@

kz_thread threads[THREAD_NUM];
kz_thread *readyque[PRI_NUM];
-static ucontext_t intr_env;
static kz_timebuf *timers;
static kz_thread *sigcalls[SIG_NUM];
static int debug_sockt = 0;
-static sigset_t block;
+sigset_t block;
+static int on_os_stack = 0;

kz_thread *current;
-static int current_signo;

static void getcurrent()
{

まず intr_env が不要になるので削除している.シグナルのブロック情報である block は,実は extintr からも利用するように修正したので static を外してある.on_os_stack については後で説明.あと current_signo はカーネル用コンテキストへの切替え後にシグナル番号を参照するための変数だったのだけど,カーネル用コンテキストへの切替え処理が無くなったのでこれも不要になったので削除.

static void thread_init(kz_thread *thp, int argc, char *argv[])
{
+ /*
+ * なぜかここで UNBLOCK にしないとシグナル受信できないので
+ * UNBLOCK にする.謎.
+ */
sigprocmask(SIG_UNBLOCK, &block, NULL);

thp->func(argc, argv);

thread_init()にはコメント追加.今回,KOZOS起動時のシグナルのブロック設定が無くなったので,上記の UNBLOCK の設定処理は不要なはず...なのだが,なぜか削るとうまく動かないので,そのまま残してある.謎だ.

次に thread_intrvec() でのディスパッチ処理.

@@ -410,19 +413,39 @@
}
extintr_proc(signo);
dispatch();
+
+ /*
+ * スタブでの read() 待ちブロック中に SIGALRM (および SIGHUP)が発生した
+ * 場合に,以下の setcontext() によるコンテキストスイッチで SIGALRM が
+ * 消えてしまう(これはスタブでのブロックに比べると発生頻度は低いが,OSの
+ * 処理中にシグナル発生した場合も同様).
+ * 対策として,シグナルのブロックを一瞬だけ開けて,SIGALRM が発生した
+ * 場合には,再度シグナル処理を行う.
+ * これはOSの割り込み処理の再入になるが,以下の位置に限定して再入が行われる
+ * ので問題は無い.
+ */
+ on_os_stack = 1;
+ sigprocmask(SIG_UNBLOCK, &block, NULL);
+ sigprocmask(SIG_BLOCK, &block, NULL);
+ on_os_stack = 0;
+
+ /* ここで SIGALRM が発生するとシグナルを取りこぼす...要検討 */
+
setcontext(&current->context.uap);
}

まあコメントにも書いてある通りなのだけど,「スタブ中の read() でのブロック中に SIGALRM が発生した場合に取りこぼすため,タイマが止まってしまう」という問題の対処として,前回はシグナル処理中のシグナル発生を受け付けるようにした.これは前回も書いた通り,KOZOSの処理中に再入する可能性が発生するため問題ありありだ.

割り込み処理中はシグナルはブロックされている(sa.sa_mask をそーいうふうに設定している)ため,setcontext() でユーザスレッドに切り替わった直後に SIGALRMが発生することを期待したいのだが,どうも setcontext() をすると,ブロック中の割り込みが消えてしまうらしく(このへんはOSに依存する現象でもあるだろう.きっと),やはり取りこぼしてしまう.

なので,OSの割り込み処理が終ってコンテキストスイッチによりユーザスレッドの動作に切り替わる直前で,sigprocmask()により一瞬だけ割り込みを開けて,SIGALRMがブロックされているならばここで受け付ける.これはKOZOSの内部処理中に再度割り込みによりKOZOSの処理が行われることになるのだが,この一瞬の部分だけなので,再入の問題は無い(KOZOS全体で割り込みを開けてしまうと,リンクリストの操作中に再入されたりするとリスト構造に矛盾が発生する,などの問題が考えられる).ただしKOZOSの内部処理中に再度割り込みによりKOZOSの処理が行われた場合には,シグナルハンドラでカレントスレッドに対するコンテキストの保存を行うとまずい.KOZOS動作用のコンテキストで,カレントスレッドのコンテキストを上書きしてしまうからだ.対策として,シグナル受け付け時には on_os_stack を立てて,シグナルハンドラ内では on_os_stack を見ることで,通常のシグナル発生か,KOZOS内部処理中の(上記割り込み開け位置での)シグナル発生かを判断できるようにしてある.

まあコメントにもあるように,実は割り込みを閉じて setcontext() が行われる短い期間に SIGALRM が発生してしまうと,やはりシグナルを取りこぼす.これについてはまたそのうち考えよう.

次はシグナルハンドラ.

static void thread_intr(int signo, siginfo_t *info, ucontext_t *uap)
{
- memcpy(&current->context.uap, uap, sizeof(ucontext_t));
- current_signo = signo;
- setcontext(&intr_env);
+ if (!on_os_stack) {
+ memcpy(&current->context.uap, uap, sizeof(ucontext_t));
+ }
+ thread_intrvec(signo);
}

さっきも書いたように,on_os_stack を見てコンテキストの保存を行う.on_os_stack がゼロでない場合は,KOZOS処理中の割り込みなので,コンテキストの保存を行う必要は無い.さらに,従来は setcontext() でカーネル用コンテキストにスイッチしてから割り込みベクタ処理を行っていたが,今回の修正でシグナル発生時に自動的にカーネル用コンテキスト(ていうか,スタック)に切り替わるようになったので,割り込みベクタ処理を直接呼んでいる.(このため current_signo という変数が不要となっている)

次に,KOZOSの起動処理だ.

static void thread_start(kz_func func, char *name, int pri, int argc, char *argv[])
{
struct sigaction sa;
+ stack_t sigstack;

memset(threads, 0, sizeof(threads));
memset(readyque, 0, sizeof(readyque));
@@ -430,20 +453,15 @@

timers = NULL;

+ sigstack.ss_sp = malloc(SIGSTKSZ);
+ sigstack.ss_size = SIGSTKSZ;
+ sigstack.ss_flags = 0;
+ sigaltstack(&sigstack, NULL);
+
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = (void (*)(int, siginfo_t *, void *))thread_intr;
- sa.sa_flags |= SA_SIGINFO;
-#if 0
+ sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
sa.sa_mask = block;
-#else
- /*
- * シグナル処理中のシグナル発生を受け付ける.
- * これを有効にするとKOZOSの処理中に再入する可能性が発生するため問題有り
- * なのだが,スタブ中での read() ブロック中に SIGALRM が発生した場合の
- * 暫定対処とする.
- */
- sigemptyset(&sa.sa_mask);
-#endif

sigaction(SIGSYS , &sa, NULL);
sigaction(SIGHUP , &sa, NULL);
@@ -460,30 +478,11 @@
current = NULL;
current = (kz_thread *)thread_run(func, name, pri, argc, argv);

- swapcontext(&intr_env, &current->context.uap);
- {
- /* なぜか getcontext() しなおさないと正常動作しない...謎 */
- static int f;
- do {
- f = 0;
- getcontext(&intr_env);
- } while (f);
- f = 1;
- }
-
- thread_intrvec(current_signo);
+ setcontext(&current->context.uap);
}

まず sigaltstack() でシグナルスタックを設定しておく.さらに sa.sa_flags に SA_ONSTACK を指定することで,シグナルスタックが利用されるようにする.シグナル処理中のシグナルは前回は暫定対処として受け付けたが,今回は受け付けないでブロックする.上でも書いたように,ディスパッチ処理の部分で一時的にシグナルを受け付けることでブロックされているシグナルを処理するように修正したからだ.

さらに,前回は swapcontext() で最初のスレッドをスタートしていた.このためシグナルハンドラ内部でカーネル用コンテキストに切替えた際にswapcontext() の直後に飛んできていたのだが,シグナルハンドラからは割り込みベクタ処理を直接呼ぶように修正したので,swapcontext() は利用せずに setcontext() で最初のスレッドをスタートするように修正する.

次に,kz_start()の修正.

void kz_start(kz_func func, char *name, int pri, int argc, char *argv[])
{
- /*
- * setjmp()/longjmp()はシグナルマスクを保存/復旧するので,
- * intr_env の setjmp() 前にシグナルマスクを設定することでシグナル処理中の
- * シグナルの発生をマスクし,割り込みハンドラ内でのシグナルを無効とする.
- * (でないと割り込みハンドラの実行中に intr_env に longjmp() した後に,
- * SIGALRM や SIGHUP の発生をハンドリングしてスレッドのディスパッチが
- * 行われてしまい,誤動作する)
- */
sigemptyset(&block);
sigaddset(&block, SIGSYS);
sigaddset(&block, SIGHUP);
@@ -492,7 +491,6 @@
sigaddset(&block, SIGSEGV);
sigaddset(&block, SIGTRAP);
sigaddset(&block, SIGILL);
- sigprocmask(SIG_BLOCK, &block, NULL);

thread_start(func, name, pri, argc, argv);

従来はカーネル用コンテキストへの切替え後にシグナルがブロックされるようにkz_start() で SIG_BLOCK に設定していたが,今回の修正でシグナル発生時には sa.sa_mask の設定により自動的にシグナルがブロックされる設定に移行するので,不要になったので削除.

あとついでに,extintr の修正.

@@ -42,6 +44,7 @@

FD_SET(cnt_fd, &readfds);
if (maxfd < cnt_fd) maxfd = cnt_fd;
+ readfds_save = readfds;

while (1) {
fds = readfds;
@@ -53,8 +56,8 @@
for (p = buf; p < buf + size; p += strlen(p) + 1) {
if (!strcmp(p, "exit"))
goto end;
- fd = atoi(p);
- FD_SET(fd, &readfds);
+ if (!strcmp(p, "refresh"))
+ readfds = readfds_save;
}
}
} else {
@@ -126,7 +129,6 @@
struct timeval tm = {0, 0};
intrbuf *ibp;
int fildes[2];
- char buf[32];
pid_t chld_pid = 0;

FD_ZERO(&readfds);
@@ -157,16 +159,14 @@
kz_send(ibp->id, size, buffer);
break;
}
- /*
- * GDBスタブ利用の場合はスタブ側で read() されてしまい
- * 上の select() で検知できないので,子プロセスへの
- * 割り込み再開通知を別の方法で毎回行う必要があるだろう.
- */
- sprintf(buf, "%d", ibp->fd);
- write(cnt_fd, buf, strlen(buf) + 1);
}
}
}
+ /*
+ * GDBスタブ利用の場合はスタブ側で read() されてしまい上の select() で
+ * 検知できないので,子プロセスへの割り込み再開通知を毎回行う.
+ */
+ write(cnt_fd, "refresh", 8);
} else if (fd) { /* from thread */
setintrbuf(fd, id, (struct sockaddr *)p);
/*

従来は,子プロセス側はソケットが読み取り可になると監視対象から外し,親プロセスのほうでソケットを読み込んだ後にソケット番号を通知し,子プロセス側で再度監視対象に加えるという処理を行っていた.しかしGDBスタブ用のソケットが読み取り可になった場合,スタブの内部で read() によって読み込まれてしまうため親プロセス側ではソケットを(すでに read() 済なので)読み取れず,ソケット番号を返さないために子プロセス側でずっと監視対象から外したままになる,という問題があった.まあスタブ用ソケットに関してはべつに子プロセスで監視してもらう必要性が薄いために問題はあまり無いのだけど,実は Ctrl-C の受け付け処理を stubd.c が行っていて,このままだとこれがうまく動作できないのだなたしか.(スタブ動作中も子プロセスはソケット監視して SIGHUP を送るので,監視対象から外れてしまう)

なので対策として,子プロセスが SIGHUP を発行した後(親プロセスがKOZOSからメッセージを受け取ったとき)にはソケットを読んだかどうかにかかわらずrefresh というコマンドを送信して,すべてのソケットを監視対処に戻すようにする.

最後に,シグナルの設定を変更.

}
pipe(fildes);
if ((chld_pid = fork()) == 0) { /* 子プロセス */
-#if 0
- signal(SIGALRM, SIG_IGN);
-#endif
+ /*
+ * 子プロセスはシグナル設定を引き継ぐらしいので,シグナル無効にする.
+ * (man sigaction に以下の記述あり)
+ * After a fork(2) or vfork(2) all signals, the signal mask, the signal
+ * stack, and the restart/interrupt flags are inherited by the child.
+ */
+ sigprocmask(SIG_BLOCK, &block, NULL);
close(fildes[1]);
intr_controller(fildes[0]);
}

まあコメントにもあるのだけど,どうも子プロセスはシグナルの設定を引き継ぐらしい.なので子プロセス側で SIGALRM とかが発生するとKOZOSの処理が行われることになり,ちょっとまずい(スレッドのディスパッチが発生してスレッドが動いてソケット操作とかしてしまうとすごくまずい).そもそも子プロセスはソケットを監視するだけでKOZOSのサービスは一切不要なので,シグナルをブロックしてしまう処理を追加する.

説明はここまで.動作は前回と同じなので説明は省略.まあとりあえず,前回と同じような操作をして同じようにgdbから操作できたことは確認した.

で,今回まででKOZOSの枠組というか,おおまかな部分が大体できあがって,必要なサービス(システムコール)もそれなりに揃っているとおもうのだけど,いったんファイル一覧を見てみよう.

% ls -l
total 118
-rw-r--r-- 1 hiroaki user 460 11 12 00:30 Makefile
-rw-r--r-- 1 hiroaki user 346 11 12 00:30 clock.c
-rw-r--r-- 1 hiroaki user 104 11 12 00:30 configure.h
-rw-r--r-- 1 hiroaki user 6465 11 12 00:31 diff.txt
-rw-r--r-- 1 hiroaki user 4441 11 12 00:30 extintr.c
-rw-r--r-- 1 hiroaki user 2073 11 12 00:30 httpd.c
-rw-r--r-- 1 hiroaki user 25110 11 12 00:30 i386-stub.c
-rw-r--r-- 1 hiroaki user 24834 11 12 00:30 i386-stub.c.orig
-rw-r--r-- 1 hiroaki user 193 11 12 00:30 idle.c
-rw-r--r-- 1 hiroaki user 1133 11 12 00:30 kozos.h
-rw-r--r-- 1 hiroaki user 657 11 12 00:30 main.c
-rw-r--r-- 1 hiroaki user 221 11 12 00:30 outlog.c
-rw-r--r-- 1 hiroaki user 1987 11 12 00:30 stubd.c
-rw-r--r-- 1 hiroaki user 3823 11 12 00:30 stublib.c
-rw-r--r-- 1 hiroaki user 252 11 12 00:30 stublib.h
-rw-r--r-- 1 hiroaki user 2739 11 12 00:30 syscall.c
-rw-r--r-- 1 hiroaki user 1538 11 12 00:30 syscall.h
-rw-r--r-- 1 hiroaki user 692 11 12 00:30 t2w.c
-rw-r--r-- 1 hiroaki user 2707 11 12 00:30 telnetd.c
-rw-r--r-- 1 hiroaki user 10715 11 12 00:30 thread.c
-rw-r--r-- 1 hiroaki user 764 11 12 00:30 thread.h
%

KOZOSのコア部分は thread.c なのだけど,たったの 764 バイト,行数は 508 行となる.まあ他にも動作に必須のものとして,外部割り込み関連が extintr.c,システムコールが syscall.c,アイドルスレッドが idle.c,スタブ処理が stubd.c と stublib.c にあるが,これらを全部合わせても行数は1113行,ヘッダファイル(configure.h, kozos.h, stublib.h, syscall.h, thread.h)を合わせても1316行なのだな.2000行にも満たないのよ.まあエラー処理とかがほとんど無いし,問題点や怪しい部分もまだまだありありなのだけど,こんなもんでOSって動いてしまうんだねえ...うーん,面白いもんだなあ.

あとは,kz_break() によるブレーク処理の修正,Ctrl-C の動作(stubd.cの動作)の説明,スレッド対応とかが必要かなあ.まあもう眠いので次回にしよう.
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

今回はなんかわかりにくいタイトルだけど,前回は,シグナル発生時のコンテキスト取得に sigaction(), SA_SIGINFO, ucontext_t を利用するように修正した.これは segmentation fault 発生時のまさに発生したその場所で gdb がブレークするための対処だった.

しかしそもそも,現状ではコンテキストの管理に setjmp()/longjmp() を利用しているが,これを getcontext()/setcontext() を利用したものに置き換えられるはずだ.前回も書いたが,setjmp()/longjmp() を使うのは古くさい書き方のようで,現在では getcontext()/setcontext() を使うべきのようだ.まあ確かに,setjmp()/longjmp() はスタックが残っていることを期待しているし(このため,setjmp()を呼び出した関数から return してはいけない.スタックが解放されてしまうから),なんつーかスタックはそのままでレジスタの状態を保存しておいて前回の場所に戻るという,目的よりも実装ありきな構造なのでなんだかなあ,という部分がある.

ということで,getcontext/setcontext() を利用した構造に書き換えてみる.とりあえずは setjmp()/longjmp() の部分をそのまま setcontext()/getcontext() に置き換えてみよう.まあいろいろと修正案はあって,将来的にはもっとシンプルな構造にしたいのだけど,とりあえずそんなかんじで修正する.前回からの差分は,いつもどおり,上記ソースコードの diff.txt 参照.

では,修正点について説明しよう.

まずは thread.h で,setjmp() 用の jmp_buf を削除する.

@@ -36,7 +35,6 @@
kz_membuf *messages;

struct {
- jmp_buf env;
ucontext_t uap;
} context;
} kz_thread;

次に thread.c の修正.

@@ -23,13 +21,14 @@

kz_thread threads[THREAD_NUM];
kz_thread *readyque[PRI_NUM];
-static jmp_buf intr_env;
+static ucontext_t intr_env;
static kz_timebuf *timers;
static kz_thread *sigcalls[SIG_NUM];
static int debug_sockt = 0;
static sigset_t block;

こちらも jmp_buf を削除し,割り込み発生時のKOZOS動作用のコンテキストとしてintr_env を ucontext_t で定義する.

次にスレッド生成の thread_run() だが,これは修正が多いので差分でなくソースコードをそのまま見て説明しよう.

static int thread_run(kz_func func, char *name, int pri,
int argc, char *argv[])
{
int i;
kz_thread *thp;

for (i = 0; i < THREAD_NUM; i++) {
thp = &threads[i];
if (!thp->id) break;
}
if (i == THREAD_NUM) return -1;

memset(thp, 0, sizeof(*thp));

thp->next = NULL;
strcpy(thp->name, name);
thp->id = thp;
thp->func = func;
thp->pri = pri;
thp->stack = malloc(SIGSTKSZ);

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

getcontext(&thp->context.uap);

thp->context.uap.uc_stack.ss_sp = thp->stack;
thp->context.uap.uc_stack.ss_size = SIGSTKSZ;
thp->context.uap.uc_stack.ss_flags = 0;
thp->context.uap.uc_link = NULL;

makecontext(&thp->context.uap, (void (*)())thread_init, 3, thp, argc, argv);

/* 起動時の初回のスレッド作成では current 未定なのでNULLチェックする */
if (current) {
putcurrent();
}

current = thp;
putcurrent();

return (int)current;
}

説明し忘れていたけど,getcontext()によるコンテキスト管理には,以下の関数がある.
  • getcontext() ... 現在の位置のコンテキストの取得.setjmp()に相当する.
  • setcontext() ... 指定したコンテキストを設定.longjmp()に相当する.
  • makecontext() ... コンテキストの作成.新しくスレッドを生成する場合に使用.
  • swapcontext() ... コンテキストの切替え.現在のコンテキストを保存し,指定したコンテキストに設定する.
まずスタックの獲得だが,今までは malloc() で獲得して setjmp() 用の jmp_buf に直接設定していたが,今回の修正でコンテキストの作成に makecontext() を利用するようになるので,malloc() して ucontext_t の ss_sp というメンバに設定している.ちなみに SIGSTKSZ は正確には sigaltstack() の推奨スタックサイズなのだが,まあ似たようなものだろうという安易な判断で,とりあえずここではスタックサイズを SIGSTKSZ としている.

さらに makecontext() でコンテキストを作成する.makecontext() は,getcontext() 呼び出し後にそのコンテキストに対してmakecontext() を行うのが正しいようなので,そういうふうに書いてある.makecontext() 呼び出し時に,スタート用の関数として thread_init() を指定し,argc, argv も指定している.前回まではスタックや jmp_buf に直接書き込むというおもいっきりCPU依存(そしてライブラリ依存)な書き方であったが,makecontext() を利用すると,こんなにすっきりと書ける.前回までと比べると,うーん,実にすっきりしたなあ.

次に,スレッドのスタート用関数である thread_init() について説明.

static void thread_init(kz_thread *thp, int argc, char *argv[])
{
sigprocmask(SIG_UNBLOCK, &block, NULL);

thp->func(argc, argv);
thread_end();
}

kz_start()内部で sigprocmask() によってシグナルをブロックしているので(これはKOZOSの内部処理中にシグナルが発生して再入するのを防止するため),スレッドのスタート時に sigprocmask() によってシグナルをアンブロックにして,シグナルを受け付けるようにしている.またスレッドのメイン関数から return してきた場合には,thread_end() によってスレッド終了している.スレッドの終了時の動作(というか,スレッドのメイン関数から return してきたときの動作)はどうやら ucontext_t の uc_link メンバを適切に設定することで制御可能なようなのだけど,面倒なのでとりあえず thread_end() を呼ぶようにしている.

次に,割り込みベクタ処理の thread_intrvec() の修正.

@@ -428,23 +410,14 @@
}
extintr_proc(signo);
dispatch();
- longjmp(current->context.env, 1);
+ setcontext(&current->context.uap);
}

スレッドのディスパッチ後(ていうか,これって言葉的にはディスパッチでなくスケジュールだね.そのうち修正しよう)に,setcontext() によりコンテキストスイッチを行っている.従来は longjmp() により前回の setjmp() した場所(これは,シグナルハンドラの内部)に戻っていた.しかし前回の修正で,シグナル発生時にシグナルハンドラの引数としてucontext_t 型でコンテキストが渡されるように修正された.今回は上記シグナルハンドラに渡されたコンテキストに setcontext() でスイッチするように修正しているので,シグナル発生したまさに直後から動作再開することになる.

実はシグナルハンドラに渡されたコンテキスト(ucontext_t)を setcontext() に渡した場合の動作はOSに依存する?らしい?のだが,FreeBSD の setcontext のマニュアルを見ると

If ucp was initialized by the invocation of a signal handler, execution
continues at the point the thread was interrupted by the signal.

という記述があって,シグナルによって割り込まれた位置から動作再開する,という記述があるのでまあこれでいいんではないかな? とする.OSによっては動作が変わってくるかもしれない.Linux ではなんか対処が必要かも.

次にシグナルハンドラだ.

static void thread_intr(int signo, siginfo_t *info, ucontext_t *uap)
{
memcpy(&current->context.uap, uap, sizeof(ucontext_t));
-
- /*
- * setjmp()/longjmp() はシグナルマスクを保存し復元するが,
- * _setjmp()/_longjmp() はシグナルマスクを保存しない.
- * (レジスタセットとスタックしか保存および復元しない)
- */
- if (setjmp(current->context.env) == 0) {
- longjmp(intr_env, signo);
- }
-
- setcontext(&current->context.uap);
+ current_signo = signo;
+ setcontext(&intr_env);
}

従来は intr_env に longjmp() することでKOZOSカーネル用コンテキストにスイッチしていた.これは,これをやらないとユーザスレッドのコンテキストがそのまま使われてしまうのを極力避けたい(とはいっても,シグナル処理に利用されてしまうので根本解決ではなく,あくまで「極力」避けることしかできない)ためなのだが,同様の理由で setcontext() によりカーネル用コンテキストに切替える.

まあ上にも書いたように,シグナル処理(シグナル発生して thread_intr() が呼ばれてから setcontext(intr_env) が行われるまでの間)にはやはりそのスレッドのスタックが利用されてしまう.これはスレッドからすると,スタックの未使用領域が突然書き変わることになり,場合によってはタイミング依存のバグの原因になり得る.たとえば

char *getstr()
{
char str[] = "sample";
return str;
}

のようなスタック上の配列を返すようなプログラムだと,まあこういうのはバグではあるのだが,実際にはスタック上にデータがしばらくは残っていて,問題なく動いてしまったりする.しかしKOZOS利用時には,シグナル発生でスタックの未使用領域が突然(予期せずに)書き変わることで,上記のようなプログラムが場合によっては動かなかったり,タイミング依存で動いたり動かなかったり,1回だけダウンしたがその後ダウン現象が発生しなかったり,というよくわからんバグの原因になる.

まあ上のプログラムは明らかにバグなので,そんなバギーなプログラムを書くほうが悪いといえばそこまでなのだけど,OSがユーザスレッドの資源を勝手に書き換えるというのも(たとえそこが未使用であるとしても)ちょっとなんだかなあという気はする.これについてはまた解決策があるので,後で修正する.

次はKOZOSの起動時の処理だ.

static void thread_start(kz_func func, char *name, int pri, int argc, char *argv[])
@@ -460,7 +433,17 @@
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = (void (*)(int, siginfo_t *, void *))thread_intr;
sa.sa_flags |= SA_SIGINFO;
+#if 0
sa.sa_mask = block;
+#else
+ /*
+ * シグナル処理中のシグナル発生を受け付ける.
+ * これを有効にするとKOZOSの処理中に再入する可能性が発生するため問題有り
+ * なのだが,スタブ中での read() ブロック中に SIGALRM が発生した場合の
+ * 暫定対処とする.
+ */
+ sigemptyset(&sa.sa_mask);
+#endif

sigaction(SIGSYS , &sa, NULL);
sigaction(SIGHUP , &sa, NULL);
@@ -477,13 +460,22 @@
current = NULL;
current = (kz_thread *)thread_run(func, name, pri, argc, argv);

- longjmp(current->context.env, 1);
+ swapcontext(&intr_env, &current->context.uap);
+ {
+ /* なぜか getcontext() しなおさないと正常動作しない...謎 */
+ static int f;
+ do {
+ f = 0;
+ getcontext(&intr_env);
+ } while (f);
+ f = 1;
+ }
+
+ thread_intrvec(current_signo);
}

thread_start()では,まあコメントにある通りなのだが,前回の問題としてgdbでのブレーク後に時刻表示が止まってしまうというものがあり,SIGALRMが捨てられてしまうことが原因なのだが,暫定対処としてsa.sa_mask を空にすることでシグナル処理中のシグナル発生を受け付ける.これはコメントにもある通り,KOZOSの内部処理中に再度シグナルが発生して再入する可能性が発生するため問題ありありなのだが(メッセージキューの張りかえ中にSIGALRM発生してメッセージキューの操作が入ると,メッセージのリンク構造に矛盾が発生して場合によってはダウンするだろう),まああくまで暫定ということで.

さらに,swapcontext()によりコンテキスト切替えを行い,カレントスレッドのコンテキストで動作開始する.これにより,直前の thread_run() で設定した最初のスレッドが起動する.swapcontext() により現在のコンテキストが intr_env に保存されるので,次回の割り込み発生時にシグナルハンドラから setcontext(intr_env) されると,swapcontext() の直後に動作が移る(つまり,カーネル用コンテキストに切り替わる).で,本来ならばそのまま thread_intrvec() を呼び出すことで割り込みハンドラに移ればいいはずなのだが,なぜか getcontext() して intr_env を設定しなおさないと正常動作しないので,そのようにしている.

最後に,KOZOSの起動処理.

void kz_start(kz_func func, char *name, int pri, int argc, char *argv[])
{
- int signo;
-
/*
* setjmp()/longjmp()はシグナルマスクを保存/復旧するので,
* intr_env の setjmp() 前にシグナルマスクを設定することでシグナル処理中の
@@ -502,15 +494,7 @@
sigaddset(&block, SIGILL);
sigprocmask(SIG_BLOCK, &block, NULL);

- /*
- * setjmp()は最低位の関数から呼ぶ必要があるので,本体は thread_start() に
- * 置いて setjmp() 呼び出し直後に本体を呼び出す.
- * (setjmp()した関数から return 後に longjmp() を呼び出してはいけない)
- */
- if ((signo = setjmp(intr_env)) == 0) {
- thread_start(func, name, pri, argc, argv);
- }
- thread_intrvec(signo);
+ thread_start(func, name, pri, argc, argv);

/* ここには返ってこない */
abort();

kz_start() はKOZOSを起動する際に一番最初に呼ばれる関数だが,従来はここで setjmp() してカーネル用コンテキストを作成していた.これはコメントにもあるように,setjmp()/longjmp() はスタックがそのまま残っていることを期待しているので,setjmp() を呼び出した関数からreturn してはいけないためにトップの関数で最初に setjmp() を行っているのだが,コンテキスト作成は swapcontext() により行うように,thread_start() に移動したため,ここではただ thread_start() を呼び出すだけになっている.まあこのへんはもうちょっと整理できそうな気がするが,とりあえずはこれでいいとしよう.

注意として,setcontext() によるコンテキストスイッチでは,シグナルのマスクやブロック,ハンドリングの設定も同時に切り替わる.つまり,シグナル処理の設定内容も,コンテキストのひとつとして管理される(これは,至極もっともなことだ).このためカーネル用コンテキストの作成前にシグナルをブロックするように設定しておき(このためカーネル用コンテキストに切り替わると,シグナルは自動的にブロックされる),スレッド作成後に thread_init() で再度シグナルをアンブロックにする,という構造になっている.このへんの設定位置を間違えると,シグナルを受けられなかったり本来受けてはいけないシグナルを受けてしまったりして,正常に動作しない原因になる(そして,きちんと理解していないとデバッグできない)ので,注意してほしい.

では,実際に動かしてみよう.まず koz を起動し,gdbで接続し,telnet で接続するまでは前回と同じ.

Sun Nov 11 22:15:19 2007
Sun Nov 11 22:15:20 2007
Sun Nov 11 22:15:21 2007
Sun Nov 11 22:15:22 2007
Sun Nov 11 22:15:23 2007
...

時刻表示が正常に開始される.

telnet から down コマンドを発行して segmentation fault を発生させてみる.

% telnet 192.168.0.3 20001
Trying 192.168.0.3...
Connected to 192.168.0.3.
Escape character is '^]'.
> down
(この状態で停止)



画像はこちら

nullp によるNULLポインタ参照でブレークしている.

nullp を(ていうか正確にはEAXレジスタを.これに関しては前回参照)telnetd_dummy に設定し直して continue する.

画像はこちら

[$m8048088,1#3e](+)($55#6a)[+]
[$M8048088,1:cc#1e](+)($OK#9a)[+]
[$C0b#d5](+)($#00)[+]
[$c#63](+)Sun Nov 11 22:16:24 2007
Sun Nov 11 22:16:25 2007
Sun Nov 11 22:16:26 2007
Sun Nov 11 22:16:27 2007
Sun Nov 11 22:16:28 2007
Sun Nov 11 22:16:29 2007
...

時刻表示が復活している.SIGALRMが正常に受信再開できているということだ.

次に,break による強制ブレークと,ステップ実行を試してみよう.

telnet から break コマンドを実行すると,kz_break() が呼ばれ,強制ブレークされる.これは実際には kill() により自分自身に SIGTRAP を発行している.実際にやってみよう.

> break
(この状態で停止)



画像はこちら

ブレークした.が,ソースコード表示(emacs の下半分ね)は以前のままだ.おそらくだが,SIGTRAPはkill()によって発行しているが,kill()はシステムコールでありアセンブラで記述されている.さらにシステムコールに関しては -g をつけてデバッグされているわけではないので,gdbがソースコードの当該の箇所を発見できず,ソースコードを表示できない(ので,以前のママ)でいるのだろう.

gdbからupを実行することで,スタックトレースを追ってひとつ上の関数呼び出しに戻る.

画像はこちら

kz_break()によりkill()した場所が表示された.

もう一度,upを実行してみよう.

画像はこちら

break コマンドを受け付けて,kz_break()を呼び出している箇所が表示されている.うんうん,スタックはうまく追えている.

ここで gdb から next を実行して,ステップ実行してみよう.

画像はこちら

さっきはkill()の位置に矢印があったのだが,kill()の次の行に進んでいることに注目.kill()呼び出しを過ぎて,kz_break() から return しようとしているようだ.

さらに next を実行.

画像はこちら

おー,kz_break() の呼び出しを過ぎて,次の処理に移っている.

さらに next を実行.

画像はこちら

ふむふむ.さらに次の処理に移っているね.

最後に continue して,動作を再開してみる.

画像はこちら

gdb は無応答になるので,うまく continue できたようだ.telnet は

> break
OK
>

となっていて,telnet からの break コマンド実行が OK で正常終了し,プロンプトが表示されている.うまく動作再開できているようだ.

[$m8048088,1#3e](+)($55#6a)[+]
[$M8048088,1:cc#1e](+)($OK#9a)[+]
[$c#63](+)Sun Nov 11 22:19:40 2007
Sun Nov 11 22:19:41 2007
Sun Nov 11 22:19:42 2007
Sun Nov 11 22:19:43 2007
Sun Nov 11 22:19:44 2007
Sun Nov 11 22:19:45 2007
...

時刻表示も復活している.

今回は setjmp()/longjmp() を利用したコンテキストスイッチをgetcontext()/setcontext() に書き換えた.またシグナルは signal() ではなく sigaction() 利用に,前回書き換えている.これらにより細かい動作のチューニングが可能になり,さらに(POSIXなので)移植性も上がるはずだ.

さらに今回は,next によるステップ実行が確認できた.これでそこそこデバッガっぽくなってきた.まあ課題はまだまだあるのだが,けっこうそれっぽい動作なのではなかろうか.ちょっと感動だね.

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

のっけから不安なタイトルなのだけど,前回の最後で,ダウン時のブレーク場所がシグナルハンドラ内の setjmp() になっていた.現状の作りではコンテキストスイッチに setjmp()/longjmp() を利用しているのでこれはどーしようもないのだけど,たとえばブレーク後にステップ実行とかしようとすると,setjmp() の直後のよくわからんところでブレークすることになってしまう.これではデバッガとしては使いものにならん.

これを避けるには,スタブにはなんとかしてシグナルが発生した瞬間の場所,というかコンテキストを拾って渡す必要があるのだけど,シグナルハンドラの内部でそれをやろうとすると,うーんどうしよう.スタックを追っかければなにか情報を取ることはできるかもしれんがCPU依存がおもいっきり出てしまうし,うーん,うーん,どうしよう.

これはとっても大問題で,実のところ,KOZOSの作成がここでかなり危ぶまれた.setjmp()/longjmp()を使うという構造上どーしようもないというか,そもそもシグナル発生の瞬間をとらえるならばOSのシグナル処理に手を入れざるを得ず,最悪,FreeBSDのカーネルに手を入れてシグナル処理の瞬間のコンテキストを取得するためのなんらかの枠組を追加できないだろうか...などと考えていた.

のだが,いろいろ調べていたら実は getcontext() というものがあって,シグナル発生の瞬間のコンテキストを取得できるんだなこれが.しかもコンテキストスイッチにも利用できるという,まさにKOZOSのために追加されたとしかおもえないよーなシステムコールがすでに存在することが判明.うーんなんてラッキーな...日頃の行いの良さだろうか.

ちなみに setjmp()/longjmp() をコンテキスト保存に使うという考えはすでにあるようで,ただしこれだとシグナルの扱いとかに制限があるので,現在はコンテキスト保存をしたいならば getcontext()/setcontext() を使うというのが良い方法のようだ.ついでにいうならシグナル発生時にコンテキスト取得をするためにはシグナルハンドラの設定に signal() ではなく sigaction() を用いて,SA_SIGINFO フラグを利用する必要がある.これに関しても,シグナル処理中のシグナル発生の制御など,sigaction()のほうが細かい設定が行えるので,現在では signal() ではなく sigaction() を使うのが良い方法のようだ.このへんはこちらに詳しいので,ぜひ参照してほしい.

さて,今回の修正なのだけど,SA_SIGINFO を利用することで,シグナル発生時のコンテキストをハンドラ側で取得することができるようになる(ハンドラ呼び出し時に引数として渡ってくる).スタブ側では,setjmp() したコンテキストでなくハンドラに渡されたコンテキストを参照するように修正する.前回からの差分は,いつもどおり,上記ソースコードの diff.txt 参照.

では,修正点について説明しよう.まず getcontext() は ucontext_t という構造体を利用するので,コンテキストの保存用に,thread.h で定義してあるスレッド構造体にucontext_t の保存領域を追加.

@@ -36,6 +37,7 @@
struct {
jmp_buf env;
+ ucontext_t uap;
} context;
} kz_thread;

これは,今まで setjmp() によるコンテキスト保存用に jmp_buf を利用していたが,それと同位置に追加することになる.スレッドのディスパッチには従来通り jmp_buf を利用するのだが,スタブには ucontext_t を渡す構造になる.まあ本来ならば setjmp/longjmp() を廃止してすべて getcontext()/setcontext() でコンテキストスイッチするような作りにすべきだろうが,とりあえず前回からの修正差分を少なくしたいので,こういう構造にした.

さらに,thread.c の修正.

-static void thread_intr(int signo)
+static void thread_intr(int signo, siginfo_t *info, ucontext_t *uap)
{
+ memcpy(&current->context.uap, uap, sizeof(ucontext_t));
+
/*
* setjmp()/longjmp() はシグナルマスクを保存し復元するが,
* _setjmp()/_longjmp() はシグナルマスクを保存しない.
@@ -439,23 +443,32 @@
if (setjmp(current->context.env) == 0) {
longjmp(intr_env, signo);
}
+
+ setcontext(&current->context.uap);
}

シグナルハンドラである thread_intr() に,siginfo_t と uxontext_t の引数を追加してある.シグナルの設定で SA_SIGINFO を立てておくと,これらの引数にシグナル発生時のコンテキスト情報が渡ってくるので,ハンドラの先頭で必要な情報を保存し,ハンドラ終了時にそのコンテキストにスイッチしている.つまりスレッドのディスパッチは従来通り longjmp() によって行われるが,その後にさらに最終的なディスパッチとして,setcontext() によるコンテキスト復帰が行われるという2段構造になっている.なんでこんなふうにしたのかというと,さっきも書いたが単に前回からの修正差分を少なくしたかったから.

KOZOSの起動時には thread_start() でシグナルの設定を行うが,従来は signal() を利用していたが sigaction() を利用し,さらにSA_SIGINFO を立てるように変更.さらにさらに,シグナル処理中にはシグナルはすべてブロックするように設定する.

static void thread_start(kz_func func, char *name, int pri, int argc, char *argv[])
{
+ struct sigaction sa;
+
memset(threads, 0, sizeof(threads));
memset(readyque, 0, sizeof(readyque));
memset(sigcalls, 0, sizeof(sigcalls));

timers = NULL;

- signal(SIGSYS, thread_intr);
- signal(SIGHUP, thread_intr);
- signal(SIGALRM, thread_intr);
- signal(SIGBUS, thread_intr);
- signal(SIGSEGV, thread_intr);
- signal(SIGTRAP, thread_intr);
- signal(SIGILL, thread_intr);
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_sigaction = (void (*)(int, siginfo_t *, void *))thread_intr;
+ sa.sa_flags |= SA_SIGINFO;
+ sa.sa_mask = block;
+
+ sigaction(SIGSYS , &sa, NULL);
+ sigaction(SIGHUP , &sa, NULL);
+ sigaction(SIGALRM, &sa, NULL);
+ sigaction(SIGBUS , &sa, NULL);
+ sigaction(SIGSEGV, &sa, NULL);
+ sigaction(SIGTRAP, &sa, NULL);
+ sigaction(SIGILL , &sa, NULL);

/*
* current 未定のためにシステムコール発行はできないので,

あとこちらは新しく追加なのだが,kz_start() でKOZOSの起動用関数が最初に呼ばれた際に,setjmp(intr_env) することでOS処理用のコンテキストを intr_env に作成していたが,以下のコメントにもあるように setjmp()/longjmp()はシグナルマスクを保存/復旧するので,なにもせずに setjmp(intr_env) してしまうと,シグナルハンドラ(thread_intr())の内部でOS処理に切替えるためにlongjmp(intr_env) した瞬間に,シグナルマスクが元に戻ってしまい,シグナルを受け付けるようになってしまう.結果としてOS処理中に SIGALRM とか SIGHUP (これはソケット監視用の子プロセスが発行する)が発生すると,再度シグナル処理に入ってしまい,誤動作する.(再入不可の場所で現象発生すると,なにが起こるかわからない.なにもおきなくても,スレッドのディスパッチが発生してしまい,スレッドが動作開始してしまう.つまりデバッグスタブ内での read() ブロック中にブロックから抜けてしまうという問題がある)

これを防止するために,setjmp(intr_env) の前にシグナルマスクを設定しておく.こうしておけば,thread_intr() 内での longjmp(intr_env) によりマスク状態に入るので,シグナルが再度発生することはなくなる.(シグナル発生による thread_intr() 呼び出し直後から thread_intr() 内での longjmp(intr_env) までの短い区間では,thread_start() 内でのシグナル設定時の ブロック設定によりマスクされる)

@@ -470,6 +483,24 @@
void kz_start(kz_func func, char *name, int pri, int argc, char *argv[])
{
int signo;
+
+ /*
+ * setjmp()/longjmp()はシグナルマスクを保存/復旧するので,
+ * intr_env の setjmp() 前にシグナルマスクを設定することでシグナル処理中の
+ * シグナルの発生をマスクし,割り込みハンドラ内でのシグナルを無効とする.
+ * (でないと割り込みハンドラの実行中に intr_env に longjmp() した後に,
+ * SIGALRM や SIGHUP の発生をハンドリングしてスレッドのディスパッチが
+ * 行われてしまい,誤動作する)
+ */
+ sigemptyset(&block);
+ sigaddset(&block, SIGSYS);
+ sigaddset(&block, SIGHUP);
+ sigaddset(&block, SIGALRM);
+ sigaddset(&block, SIGBUS);
+ sigaddset(&block, SIGSEGV);
+ sigaddset(&block, SIGTRAP);
+ sigaddset(&block, SIGILL);
+ sigprocmask(SIG_BLOCK, &block, NULL);

/*
* setjmp()は最低位の関数から呼ぶ必要があるので,本体は thread_start() に

最後に,スタブで従来は setjmp() 用の jmp_buf からレジスタ情報を取得していた部分を,ucontext_t から取得するように修正.

diff -ruN kozos11/stublib.c kozos12/stublib.c
--- kozos11/stublib.c Sat Nov 10 16:10:31 2007
+++ kozos12/stublib.c Sat Nov 10 16:10:33 2007
@@ -100,22 +100,43 @@
void stub_store_regs(kz_thread *thp)
{
memset(registers, 0, sizeof(registers));
- registers[PC] = thp->context.env[0]._jb[0]; /* EIP */
- registers[EBX] = thp->context.env[0]._jb[1]; /* EBX */
- registers[ESP] = thp->context.env[0]._jb[2]; /* ESP */
- registers[EBP] = thp->context.env[0]._jb[3]; /* EBP */
- registers[ESI] = thp->context.env[0]._jb[4]; /* ESI */
- registers[EDI] = thp->context.env[0]._jb[5]; /* EDI */
+
+ registers[EAX] = thp->context.uap.uc_mcontext.mc_eax;
+ registers[ECX] = thp->context.uap.uc_mcontext.mc_ecx;
+ registers[EDX] = thp->context.uap.uc_mcontext.mc_edx;
+ registers[EBX] = thp->context.uap.uc_mcontext.mc_ebx;
+ registers[ESP] = thp->context.uap.uc_mcontext.mc_esp;
+ registers[EBP] = thp->context.uap.uc_mcontext.mc_ebp;
+ registers[ESI] = thp->context.uap.uc_mcontext.mc_esi;
+ registers[EDI] = thp->context.uap.uc_mcontext.mc_edi;
+ registers[PC] = thp->context.uap.uc_mcontext.mc_eip;
+ registers[PS] = thp->context.uap.uc_mcontext.mc_eflags;
+ registers[CS] = thp->context.uap.uc_mcontext.mc_cs;
+ registers[SS] = thp->context.uap.uc_mcontext.mc_ss;
+ registers[DS] = thp->context.uap.uc_mcontext.mc_ds;
+ registers[ES] = thp->context.uap.uc_mcontext.mc_es;
+ registers[FS] = thp->context.uap.uc_mcontext.mc_fs;
+ registers[GS] = thp->context.uap.uc_mcontext.mc_gs;
}

void stub_restore_regs(kz_thread *thp)
{
- thp->context.env[0]._jb[0] = registers[PC]; /* EIP */
- thp->context.env[0]._jb[1] = registers[EBX]; /* EBX */
- thp->context.env[0]._jb[2] = registers[ESP]; /* ESP */
- thp->context.env[0]._jb[3] = registers[EBP]; /* EBP */
- thp->context.env[0]._jb[4] = registers[ESI]; /* ESI */
- thp->context.env[0]._jb[5] = registers[EDI]; /* EDI */
+ thp->context.uap.uc_mcontext.mc_eax = registers[EAX];
+ thp->context.uap.uc_mcontext.mc_ecx = registers[ECX];
+ thp->context.uap.uc_mcontext.mc_edx = registers[EDX];
+ thp->context.uap.uc_mcontext.mc_ebx = registers[EBX];
+ thp->context.uap.uc_mcontext.mc_esp = registers[ESP];
+ thp->context.uap.uc_mcontext.mc_ebp = registers[EBP];
+ thp->context.uap.uc_mcontext.mc_esi = registers[ESI];
+ thp->context.uap.uc_mcontext.mc_edi = registers[EDI];
+ thp->context.uap.uc_mcontext.mc_eip = registers[PC];
+ thp->context.uap.uc_mcontext.mc_eflags = registers[PS];
+ thp->context.uap.uc_mcontext.mc_cs = registers[CS];
+ thp->context.uap.uc_mcontext.mc_ss = registers[SS];
+ thp->context.uap.uc_mcontext.mc_ds = registers[DS];
+ thp->context.uap.uc_mcontext.mc_es = registers[ES];
+ thp->context.uap.uc_mcontext.mc_fs = registers[FS];
+ thp->context.uap.uc_mcontext.mc_gs = registers[GS];
}

では,実際に動かしてみよう.まず koz を起動し,gdbで接続するまでは前回と同じ.で,gdbで接続して continue すると

./t2w koz
% ./koz
($T054:542e0e08;5:702e0e08;8:17e70508;#82)[+]
[$Hc-1#09](+)($#00)[+]
[$qC#b4](+)($#00)[+]
[$qOffsets#4b](+)($#00)[+]
[$?#3f](+)($S05#b8)[+]
[$Hg0#df](+)($#00)[+]
[$g#67](+)($0000000000000000e003000001000000542e0e08702e0e0804e8bfbf0000000017e7050806020000330000003b0000003b0000003b0000003b0000001b000000#a8)[+]
[$qSymbol::#5b](+)($#00)[+]
[$Z0,8048088,1#87](+)($#00)[+]
[$m8048088,1#3e](+)($55#6a)[+]
[$X8048088,0:#62](+)($#00)[+]
[$M8048088,1:cc#1e](+)($OK#9a)[+]
[$vCont?#49](+)($#00)[+]
[$Hc0#db](+)($#00)[+]
[$c#63](+)Sat Nov 10 16:03:50 2007
Sat Nov 10 16:03:51 2007
Sat Nov 10 16:03:52 2007
Sat Nov 10 16:03:53 2007
Sat Nov 10 16:03:54 2007
Sat Nov 10 16:03:55 2007
Sat Nov 10 16:03:57 2007
Sat Nov 10 16:03:58 2007
Sat Nov 10 16:03:59 2007
...

のようになって時刻表示が動作開始する.

telnet で繋いでみよう.

% telnet 192.168.0.3 20001
Trying 192.168.0.3...
Connected to 192.168.0.3.
Escape character is '^]'.
> date
Sat Nov 10 16:04:37 2007
OK
>

正常に接続できた.

down コマンドを実行して,segmentation fault を発生させてみる.

> down
(この状態で停止)

このときのgdbの状態は以下.

画像はこちら

おー,ちゃんとゼロアドレス書き込みの部分で停止している.

ここでgdbで,nullpの値を見てみよう.

(gdb) print nullp
$1 = (int *) 0x0
(gdb)

うんうん,NULLになっているようだ.

nullp を別の値に書き換えて continue してみる.

(gdb) print nullp = &telnetd_dummy
$1 = (int *) 0x80c8b1c
(gdb) continue



画像はこちら

ありゃ,またダウンしてしまった.

うーんへんだ.レジスタの値を見てみよう.

(gdb) info registers
eax 0x0 0
ecx 0x80a726f 134902383
edx 0x0 0
ebx 0x1 1
...
(gdb)



EAXがゼロになっている.これが nullp の値を引き継いでいるのではなかろうか.

再びダウンした原因なんだけど,nullp を NULL でなく 0x555 とかの適当な値に初期化するようにtelnetd.c を書き換えて実行してみるとわかるのだが,どうやら nullp の値がすでに EAX レジスタに代入されていて,nullp の値を書き換えてもダメで,EAXを書き換えないとダメなようだ.

(gdb) print $eax = &telnetd_dummy
$2 = 135039772
(gdb)

で,continueしてみる.

画像はこちら

おお,こんどはうまく continue できたようだ.telnet側では

> down
OK
>

のようにして,downコマンドが正常に終了してプロンプトが出ている.うん,ちゃんと動作続行できている.

ところが,残念ながらここで時刻表示が停止してしまっているのだな.つまり clock スレッドが止まってしまっているわけだ.

原因なのだけど,スタブでのブロック中に SIGALRM が発生してしまうのだが,setcontext()でのコンテキスト復旧時にシグナル発生前のコンテキストに戻ってしまうため,SIGALRMシグナルが捨てられてしまっているからだ(SIGALRMのハンドリング処理で alarm() により再度タイマをかけるので,いったん SIGALRM が捨てられると,そのままタイマ動作が全停止してしまう).まあ,とりあえずgdbで正常にブレーク&cotninueできたわけだし,今回はとりあえずここまで.この対処は次回!