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

前回までで 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を格納するような対処も行う.

で,実装したのがこんな感じ.前回からの差分については 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 *)&registers[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のスレッドのときのみ,反応する.これはどんなふうに実現されているかというと,
  • 通常通り,ブレークポイントを設定する.
  • ブレーク時にgdbはカレントスレッドを聞きにいき,当該のスレッドならばブレークする.
  • 当該のスレッドでないならば,そのブレークは無視して,動作続行する.
というような処理が行われる.まあ実際には指定されたスレッドでない場合にも,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 コマンドが発行されているので,ブレークしているのは確かだ.ただその後,以下の動作が行われているようだ.
  • Mコマンドによりブレークポイントを元に戻す.
  • sコマンドによりステップ実行で処理をちょい先に進める.
  • Mコマンドにより再度ブレークポイントを設定する.
  • cコマンドにより continue する.
つまり,ブレークポイントを無視して continue しているのである.

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

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