(第15回)Ctrl-Cブレーク対応

2007/11/12

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

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

前回,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() を呼ぶような実装に修正する.これでブレーク時にソースコードが表示されるはずだ.

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

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

まず 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が処理を奪うことが できるようになっているのだ.


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