(第22回)GDBのスレッド対応(その5:スレッド切替えとまとめ)

2007/11/24

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

前回までで info threads によるスレッド情報表示ができるようになった. 次は thread コマンドによるスレッド切替えだ. ついでに前回問題になった,info threads を2回行うと固まるというバグを 修正したい.

まずgdb上で info threads を行うと

(gdb) info threads
  7 Thread 135026304 ( Name: httpd, State: SLP, Priority: 09)  0x0805edeb in kill ()
  6 Thread 135025536 ( Name: telnetd, State: SLP, Priority: 08)  0x0805edeb in kill
    ()
  5 Thread 135024768 ( Name: clock, State: SLP, Priority: 07)  0x0805edeb in kill ()
  4 Thread 135024000 ( Name: idle, State: RUN, Priority: 1f)  0x0806088f in select ()
  3 Thread 135023232 ( Name: outlog, State: SLP, Priority: 03)  0x0805edeb in kill ()
* 2 Thread 135022464 ( Name: stubd, State: RUN, Priority: 02)  breakpoint ()
    at i386-stub.c:1158
  1 Thread 135021696 ( Name: extintr, State: SLP, Priority: 01)  0x0805edeb in kill
    ()
(gdb) 
のようにしてスレッド情報が表示されるが,このとき一番左に表示されている 1〜7の数値が,スレッド番号になる.gdb上でスレッド操作する場合には, この番号によってスレッドを指定する.

たとえばここで

(gdb) thread 3
とかを行うと,カレントスレッドをスレッド番号3である outlog スレッドに 切替える.で,where とかやると,そのスレッドのスタックの状態が見れることに なる.

で,前回の実装でためしに thread コマンドによるスレッド切替えを実行してみたら, 以下のようになった.

(gdb) thread 3
Thread ID 3 has terminated.

(gdb)
このときgdbとスタブの間では,以下の通信が行われている.
[$T080c4a80#4c](+)($#00)[+]
Tコマンドというのがgdbから送られてきているが,現状のスタブでは実装されて いないので「$#00」を返している.で,スレッド切替えに失敗しているようだ. ということで,Tコマンドに対して応答するように修正すればいいようだ.

で,いつもどおりremote.cを調べてみる. 「'T'」で検索してもそれっぽい部分が見つからなかったので, 「"T」で検索したら以下の部分が見つかった.

static int
remote_thread_alive (ptid_t ptid)
{
  struct remote_state *rs = get_remote_state ();
  int tid = PIDGET (ptid);

  if (tid < 0)
    xsnprintf (rs->buf, get_remote_packet_size (), "T-%08x", -tid);
  else
    xsnprintf (rs->buf, get_remote_packet_size (), "T%08x", tid);
  putpkt (rs->buf);
  getpkt (&rs->buf, &rs->buf_size, 0);
  return (rs->buf[0] == 'O' && rs->buf[1] == 'K');
}
関数名から察するに,スレッドの存在を調べているようだ. 見たところ,スレッドが存在する場合には"OK"を返せばいいようなので そーいうふうに実装してみよう.

remote.c を読んだ感じだと,スレッドの存在/消去は適当に remote_thread_alive() が呼ばれることで検出されるようだ. 消滅したスレッドは「T」コマンドでOK以外が返ることになり, その場合にはスレッドは消滅したと判断して削除する, という処理になっているようだ.

さらに今回,カレントスレッドの切替えを追っていて気がついたのだが, シグナル発生時のスタブ側のシグナル送信処理(handle_exception()の先頭付近)で, Tコマンドと一緒にスレッド番号を送信することで,カレントスレッドを gdb 側に 伝えることができるようだ (さらに新規スレッドの場合には,[New スレッドID]のように表示される). ここで言うTコマンドと言うのは,上で説明したgdb側からスレッドの存在確認のために gdbから送られてくるTコマンドではなく,ブレーク時などにスタブ側から送信される ものだ(第16回参照). 今回はTコマンドがgdb→スタブのものとスタブ→gdbのものの2種類が登場するので, 混同しないように注意してほしい.

gdb側がウエイト状態(スタブのブレーク待ちで,gdbプロンプト未表示状態)では, remote.c の remote_wait() によってブレーク待ちとなっている. で,スタブからTコマンドが送られてくるとブレークが発生したと判断して gdbの処理が始まるのだが,ここで

                    if (strncmp (p, "thread", p1 - p) == 0)
                      {
                        p_temp = unpack_varlen_hex (++p1, &thread_num);
                        record_currthread (thread_num);
                        p = p_temp;
                      }
のようにして,"thread" という文字列を見ている部分がある. "thread" に続いてスレッドIDを送ることで,カレントスレッドを教えることができる ようなのだ.

さらにこの際,record_currthread() という関数が呼ばれている. record_currthread() は以下のようになっている.

static void
record_currthread (int currthread)
{
  general_thread = currthread;

  /* If this is a new thread, add it to GDB's thread list.
     If we leave it up to WFI to do this, bad things will happen.  */
  if (!in_thread_list (pid_to_ptid (currthread)))
    {
      add_thread (pid_to_ptid (currthread));
      ui_out_text (uiout, "[New ");
      ui_out_text (uiout, target_pid_to_str (pid_to_ptid (currthread)));
      ui_out_text (uiout, "]\n");
    }
}
パッと見た感じだと,カレントスレッドを切替えて,さらにそれが新しいスレッド (既存のスレッドのリスト中に存在しない)ならば,[New ...]というメッセージを 表示しているようだ.

なので,今回はスタブのブレーク時のTコマンド送信部分に,スレッドIDを 格納するような対処も行う.

で,実装したのがこんな感じ.

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

前回からの差分については diff.txt 参照.

まず,

diff -ruN kozos21/i386-stub.c kozos22/i386-stub.c
--- kozos21/i386-stub.c	Sat Nov 24 11:35:14 2007
+++ kozos22/i386-stub.c	Sat Nov 24 14:39:50 2007
@@ -831,6 +831,11 @@
   ptr = mem2hex((char *)®isters[PC], ptr, 4, 0); 	/* PC */
   *ptr++ = ';';
 
+  strcpy(ptr, "thread:");
+  ptr += 7;
+  ptr = intNToHex(ptr, (int)gen_thread->id, 4);
+  *ptr++ = ';';
+
   *ptr = '\0';
 
   putpacket (remcomOutBuffer);
という部分で,スタブのブレーク時にTコマンドを発行する際に, レジスタ情報をいくつかパラメータとして追加した後, 「thread:(スレッドID);」のようなパラメータを追加している. これによりgdb側でカレントスレッドを認識できる. これはスタブからgdbへのTコマンドだ.

さらに,こちらはgdbからスタブに対するTコマンド

 #endif
 	  break;
 
+	case 'T':
+	  {
+	    int threadid;
+	    kz_thread *thp;
+	    hexToInt(&ptr, &threadid);
+	    thp = (kz_thread *)threadid;
+	    if (thp->id) {
+	      strcpy (remcomOutBuffer, "OK");
+	    } else {
+	      strcpy (remcomOutBuffer, "E01");
+	    }
+	  }
+	  break;
+
 	case 'q':
 	  switch (*ptr++)
 	    {
スタブ側はgdbからTコマンドが送られてきた場合には, そのスレッドの存在の有無を調べてOK or エラーを返している. remote.c を読む限り,エラーは何を返してもよさそうだが, とりあえず E01 を返している.

次に「qP」コマンドに対する修正.前回,スレッドIDが 0xffffffff でqPコマンドが 送られてくることの対処だ.

@@ -1029,7 +1048,16 @@
 		mode = hexToIntN(&ptr, 4);
 		threadid[0] = hexToIntN(&ptr, 4);
 		threadid[1] = hexToIntN(&ptr, 4);
-		thp = (kz_thread *)threadid[1];
+		if (threadid[1] == 0xffffffff)
+		  {
+		    /*
+		     * 何を返すべきかちょっと不明なので,とりあえず
+		     * カレントスレッドを返す.
+		     */
+		    thp = gen_thread; /* current を返すべきか? 不明... */
+		  } else {
+		    thp = (kz_thread *)threadid[1];
+		  }
 
 		ptr = remcomOutBuffer;
 		*ptr++ = 'Q';
@@ -1041,13 +1069,13 @@
 		if (mode & TAG_THREADID) {
 		  ptr = intNToHex(ptr, TAG_THREADID, 4); /* mode */
 		  ptr = intNToHex(ptr, 16, 1); /* length */
-		  ptr = intNToHex(ptr, threadid[0], 4);
-		  ptr = intNToHex(ptr, threadid[1], 4);
+		  ptr = intNToHex(ptr, 0, 4);
+		  ptr = intNToHex(ptr, (int)thp->id, 4);
 		}
 		if (mode & TAG_EXISTS) {
 		  ptr = intNToHex(ptr, TAG_EXISTS, 4); /* mode */
 		  ptr = intNToHex(ptr, 1, 1); /* length */
-		  *ptr++ = '1';
+		  *ptr++ = thp->id ? '1' : '0';
 		}
 		if (mode & TAG_DISPLAY) {
 		  ptr = intNToHex(ptr, TAG_DISPLAY, 4); /* mode */
実は gdb のソースをちょっと追いかけたのだが,どんなときにスレッドIDが 0xffffffff として qP が発行されるのかが,ちょっとわからなかった. まあほんとはそんなあまっちょろいこと言ってないでちゃんと調べなければ ならないのだが,とりあえずカレントスレッドを返してみる.

注意しなければならないのは,「qP」コマンドの応答時には, remote_unpack_thread_info_response()でスレッドIDのチェックをしているので, スレッドIDは送られてきた 0xffffffff をそのまま返して, TAG_THREADID によってカレントスレッドのスレッドIDを返しているという点だ. あとついでに TAG_EXISTS によってスレッドの存在の有無を返す際に, スレッドの存在をちゃんと調べるように修正.

最後に,Hgコマンドに対する修正.

@@ -1098,6 +1126,7 @@
 		  {
 		    if (rev) val = -val;
 		    stub_restore_regs(gen_thread);
+		    if (val == -1) val = (int)current;
 		    gen_thread = (kz_thread *)val;
 		    stub_store_regs(gen_thread);
 		    strcpy (remcomOutBuffer, "OK");
実は Hg コマンドによるスレッド切替えがすでに実装されているため, gdb での thread コマンドによるスレッド切替え動作は, スタブ側で今回新規に実装する必要は無い. ただ,Hg に関しても
Hg-1
のようにしてスレッドIDが 0xffffffff で送られてくる場合があるようなので, これもとりあえずカレントスレッドにするように対処してみた.

では,動かしてみよう.いつもどおり実行形式 koz を起動,gdbで接続, continue,Ctrl-Cブレークしてから,info threads を実行してみる.

[New Thread 135022688] と表示されていて,新しいスレッドが作成されたことが 検知できていることに注目.

info threads を実行してみる.

無事に表示されている.もう一度 info threads を実行してみよう.

おー,2回繰り返しても問題無くなった.

次に,thread コマンドによってスレッドを切替えてみる.

で,where によりスタックトレースをとってみよう.

ふむ,outlog_main() から呼び出されているので,たしかに outlog スレッドの トレースのようだ.こんなふうにして,ブレーク時の各スレッドの情報を見ることが できる.当然だけど up コマンドでスタックトレースを追いかければ,関数呼び出し の階層を追うことができる.ソースコードももちろんその都度表示される.

ちなみにブレークポイントもスレッド単位で設定できて,たとえば

(gdb) break func thread 3
みたいに設定すれば,そのブレークポイントはスレッド番号3のスレッドのときのみ, 反応する.これはどんなふうに実現されているかというと, というような処理が行われる.まあ実際には指定されたスレッドでない場合にも, CPUはトラップ命令を実行してブレークが発生しているのだけど, gdbがうまく無視して先に進めてくれているわけだ. このへんの処理は,gdbとスタブの通信内容を見てみると gdbが実によろしくやってくれていることがわかり,非常に面白い.

ちょっと試してみよう.まず,ブレークポイントの設定.

(gdb) break dummy_func thread 3
Breakpoint 1 at 0x804b11e: file telnetd.c, line 21.
(gdb) 
第16回で説明したことでちょっと忘れているかもしれないけど, telnet で接続して call を実行することで,dummy_func() というダミー関数が 呼ばれるようになっている. で,dummy_func() に対してスレッド番号3(outlogスレッド)でブレークポイントを 張ってみた.

実際に telnet から call を実行してみよう.

% telnet 192.168.0.3 20001
Trying 192.168.0.3...
Connected to 192.168.0.3.
Escape character is '^]'.
> call
OK
> 
ブレークポイントが張ってある dummy_func() が呼ばれたにもかかわらず, ブレークせずに正常終了している.このときのgdbとスタブの通信は以下.
...
Sat Nov 24 14:08:43 2007
Sat Nov 24 14:08:44 2007
Sat Nov 24 14:08:45 2007
($T054:ec561208;5:f4561208;8:1fb10408;thread:080c4260;#31)[+]
[$g#67](+)($00000000477a0a080000000060420c08ec561208f4561208ec571208000000001fb1040812020000330000003b0000003b0000003b0000003b0000001b000000#98)[+]
[$P8=1eb10408#ba](+)($OK#9a)[+]
[$M8048088,1:55#c2](+)($OK#9a)[+]
[$M804b11e,1:c7#43](+)($OK#9a)[+]
[$Hc80c4260#42](+)($#00)[+]
[$s#73](+)($T054:ec561208;5:f4561208;8:25b10408;thread:080c4260;#01)[+]
[$m8048088,1#3e](+)($55#6a)[+]
[$M8048088,1:cc#1e](+)($OK#9a)[+]
[$m804b11e,1#8f](+)($c7#9a)[+]
[$M804b11e,1:cc#6f](+)($OK#9a)[+]
[$Hc0#db](+)($#00)[+]
[$c#63](+)Sat Nov 24 14:08:47 2007
Sat Nov 24 14:08:48 2007
Sat Nov 24 14:08:49 2007
Sat Nov 24 14:08:50 2007
...
まず最初に T コマンドが発行されているので,ブレークしているのは確かだ. ただその後,以下の動作が行われているようだ. つまり,ブレークポイントを無視して continue しているのである.

ステップ実行で処理を進めるのは,continue の直後に同じ場所でまたブレークしない ようにするためのおなじみの動作だ.このへんについては第16回を参照.

うーん,さすがgdb.こんなことまでしてくれるんだねえ...ちょっと感動.


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