(第12回)ダウンした場所でブレークさせるにはどうしよう?

2007/11/10

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

のっけから不安なタイトルなのだけど,前回の最後で,ダウン時のブレーク場所が シグナルハンドラ内の 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() したコンテキストでなくハンドラに渡されたコンテキストを 参照するように修正する.

(2009/04/10 ライセンスに関する文書として,KL-01とLICENSEを追加. 詳しくは第43回を参照)

前回からの差分は,いつもどおり,上記ソースコードの 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(¤t->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(¤t->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できたわけだし, 今回はとりあえずここまで.この対処は次回!


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