(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.
のっけから不安なタイトルなのだけど,前回の最後で,ダウン時のブレーク場所がシグナルハンドラ内の 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 の保存領域を追加.
これは,今まで setjmp() によるコンテキスト保存用に jmp_buf を利用していたが,それと同位置に追加することになる.スレッドのディスパッチには従来通り jmp_buf を利用するのだが,スタブには ucontext_t を渡す構造になる.まあ本来ならば setjmp/longjmp() を廃止してすべて getcontext()/setcontext() でコンテキストスイッチするような作りにすべきだろうが,とりあえず前回からの修正差分を少なくしたいので,こういう構造にした.
さらに,thread.c の修正.
シグナルハンドラである thread_intr() に,siginfo_t と uxontext_t の引数を追加してある.シグナルの設定で SA_SIGINFO を立てておくと,これらの引数にシグナル発生時のコンテキスト情報が渡ってくるので,ハンドラの先頭で必要な情報を保存し,ハンドラ終了時にそのコンテキストにスイッチしている.つまりスレッドのディスパッチは従来通り longjmp() によって行われるが,その後にさらに最終的なディスパッチとして,setcontext() によるコンテキスト復帰が行われるという2段構造になっている.なんでこんなふうにしたのかというと,さっきも書いたが単に前回からの修正差分を少なくしたかったから.
KOZOSの起動時には thread_start() でシグナルの設定を行うが,従来は signal() を利用していたが sigaction() を利用し,さらにSA_SIGINFO を立てるように変更.さらにさらに,シグナル処理中にはシグナルはすべてブロックするように設定する.
あとこちらは新しく追加なのだが,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() 内でのシグナル設定時の ブロック設定によりマスクされる)
最後に,スタブで従来は setjmp() 用の jmp_buf からレジスタ情報を取得していた部分を,ucontext_t から取得するように修正.
では,実際に動かしてみよう.まず koz を起動し,gdbで接続するまでは前回と同じ.で,gdbで接続して continue すると
のようになって時刻表示が動作開始する.
telnet で繋いでみよう.
正常に接続できた.
down コマンドを実行して,segmentation fault を発生させてみる.
このときのgdbの状態は以下.
画像はこちら
おー,ちゃんとゼロアドレス書き込みの部分で停止している.
ここでgdbで,nullpの値を見てみよう.
うんうん,NULLになっているようだ.
nullp を別の値に書き換えて continue してみる.
画像はこちら
ありゃ,またダウンしてしまった.
うーんへんだ.レジスタの値を見てみよう.
EAXがゼロになっている.これが nullp の値を引き継いでいるのではなかろうか.
再びダウンした原因なんだけど,nullp を NULL でなく 0x555 とかの適当な値に初期化するようにtelnetd.c を書き換えて実行してみるとわかるのだが,どうやら nullp の値がすでに EAX レジスタに代入されていて,nullp の値を書き換えてもダメで,EAXを書き換えないとダメなようだ.
で,continueしてみる.
画像はこちら
おお,こんどはうまく continue できたようだ.telnet側では
のようにして,downコマンドが正常に終了してプロンプトが出ている.うん,ちゃんと動作続行できている.
ところが,残念ながらここで時刻表示が停止してしまっているのだな.つまり clock スレッドが止まってしまっているわけだ.
原因なのだけど,スタブでのブロック中に SIGALRM が発生してしまうのだが,setcontext()でのコンテキスト復旧時にシグナル発生前のコンテキストに戻ってしまうため,SIGALRMシグナルが捨てられてしまっているからだ(SIGALRMのハンドリング処理で alarm() により再度タイマをかけるので,いったん SIGALRM が捨てられると,そのままタイマ動作が全停止してしまう).まあ,とりあえずgdbで正常にブレーク&cotninueできたわけだし,今回はとりあえずここまで.この対処は次回!
のっけから不安なタイトルなのだけど,前回の最後で,ダウン時のブレーク場所がシグナルハンドラ内の 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(¤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できたわけだし,今回はとりあえずここまで.この対処は次回!