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

えー,こないだ本屋で初めて見たのだけど,CQ出版からgdbのTECH-Iが出ているね.

「GDBを使った実践的デバッグ手法」

とりあえず買って内容を見てみたけど,スタブの移植の話題もあり,うん,ためになる.

あと30日でできる! OS自作入門というわりと有名な本があって,買ってみた.うーん,ためになるなあ.というか著者の「あんまり難しいこと考えないでパッと面白いもの作ってみよう」的な考え方には賛成だなあ.まあ実際のところ,OSを作るのはそんなに難しくはないとは思うのだけど,細かいことを考え出したらきりがない.なので,とりあえず動くものを作ってしまっていじりながら勉強していく,というのはとてもいいと思う.

で,KOZOSなのだが,前回までで,スレッド情報が見れるようにはなった.が,せめてスレッド名くらいは出てくれないとさみしいよね.ということで,今回はスレッドの詳細情報の表示について.

前回実装しなかったコマンドとして
  • qThreadExtraInfo,80c4560
  • qP0000001f00000000080c4560
というのがあった.まあどちらもスレッドの詳細情報を取得するためのコマンドのようなのだが,前回ではとりあえず未実装として「$#00」を返していたために,gdb側ではスレッド名などを表示できないでいた.今回はこのへんを実装してみる.

まあまずはいつもどおり,remote.cを読んでみよう.

前回もちょっと説明したが,これらのコマンドは remote.c のqThreadExtraInfo は remote_threads_extra_info() で,qP は pack_threadinfo_request() で発行されている.で,これも前回説明したけどremote_threads_extra_info() で,qThreadExtraInfo に失敗した際にremote_get_threadinfo() を経由して pack_threadinfo_request() が呼ばれている.ということは,「qThreadExtraInfo」は新しいスレッド情報取得コマンド,「qP」レガシーなスレッド情報取得コマンドだと思われる.ということで,とりあえず「qP」に応答するように実装してみよう.

で,すでに説明したが「qP」は remote_get_threadinfo() 経由でpack_threadinfo_request() で発行されている.

static int
remote_get_threadinfo (threadref *threadid, int fieldset, /* TAG mask */
struct gdb_ext_thread_info *info)
{
struct remote_state *rs = get_remote_state ();
int result;

pack_threadinfo_request (rs->buf, fieldset, threadid);
putpkt (rs->buf);
getpkt (&rs->buf, &rs->buf_size, 0);
result = remote_unpack_thread_info_response (rs->buf + 2,
threadid, info);
return result;
}

「qP」の発行後に remote_unpack_thread_info_response() が呼ばれ,応答が解析されるようだ.remote_get_threadinfo()の発行元である remote_threads_extra_info()では

static char *
remote_threads_extra_info (struct thread_info *tp)
{
...
if (use_threadextra_query)
{
xsnprintf (rs->buf, get_remote_packet_size (), "qThreadExtraInfo,%x",
PIDGET (tp->ptid));
putpkt (rs->buf);
getpkt (&rs->buf, &rs->buf_size, 0);
if (rs->buf[0] != 0)
...

のようにして,まずは「qThreadExtraInfo」を試した後,スタブ側で未実装(つまり,「$#00」が返ってきた)であるならば

set = TAG_THREADID | TAG_EXISTS | TAG_THREADNAME
| TAG_MOREDISPLAY | TAG_DISPLAY;
int_to_threadref (&id, PIDGET (tp->ptid));
if (remote_get_threadinfo (&id, set, &threadinfo))
if (threadinfo.active)
{
if (*threadinfo.shortname)
n += xsnprintf (&display_buf[0], sizeof (display_buf) - n,
" Name: %s,", threadinfo.shortname);
if (*threadinfo.display)
n += xsnprintf (&display_buf[n], sizeof (display_buf) - n,
" State: %s,", threadinfo.display);
if (*threadinfo.more_display)
n += xsnprintf (&display_buf[n], sizeof (display_buf) - n,
" Priority: %s", threadinfo.more_display);

のようにして remote_get_threadinfo() を呼び出して「qP」を発行,応答を解析し,スレッド名,状態(State),優先度(Priority)を表示しているようだ.

ということで,まあ結論からすると「qP」のgdb→スタブへの送信方法は pack_threadinfo_request(),スタブ→gdbの応答方法は remote_unpack_thread_info_response() を見ればよい.

static char *
pack_threadinfo_request (char *pkt, int mode, threadref *id)
{
*pkt++ = 'q'; /* Info Query */
*pkt++ = 'P'; /* process or thread info */
pkt = pack_int (pkt, mode); /* mode */
pkt = pack_threadid (pkt, id); /* threadid */
*pkt = '\0'; /* terminate */
return pkt;
}

前回は「qP」は

qP0000001f00000000080c4560

のように送信されていた.pack_threadinfo_request()を見ると,qPに続く16進文字の前半8桁は mode,後半16桁はスレッドIDということになる.スレッドIDは,おそらく情報を取得したいスレッドのスレッドIDだと想像がつく.問題は mode だが,これは pack_threadinfo_request() の呼び出しを遡ると,remote_threads_extra_info() の内部で

set = TAG_THREADID | TAG_EXISTS | TAG_THREADNAME
| TAG_MOREDISPLAY | TAG_DISPLAY;

のようにして設定した値が渡されてくるようだ.

次にqPの応答解析部分.

static int
remote_unpack_thread_info_response (char *pkt, threadref *expectedref,
struct gdb_ext_thread_info *info)
{
struct remote_state *rs = get_remote_state ();
int mask, length;
int tag;
threadref ref;
char *limit = pkt + rs->buf_size; /* Plausible parsing limit. */
int retval = 1;

/* info->threadid = 0; FIXME: implement zero_threadref. */
info->active = 0;
info->display[0] = '\0';
info->shortname[0] = '\0';
info->more_display[0] = '\0';

/* Assume the characters indicating the packet type have been
stripped. */
pkt = unpack_int (pkt, &mask); /* arg mask */
pkt = unpack_threadid (pkt, &ref);

if (mask == 0)
warning (_("Incomplete response to threadinfo request."));
if (!threadmatch (&ref, expectedref))
{ /* This is an answer to a different request. */
warning (_("ERROR RMT Thread info mismatch."));
return 0;
}
copy_threadref (&info->threadid, &ref);

/* Loop on tagged fields , try to bail if somthing goes wrong. */

/* Packets are terminated with nulls. */
while ((pkt < limit) && mask && *pkt)
{
pkt = unpack_int (pkt, &tag); /* tag */
pkt = unpack_byte (pkt, &length); /* length */
if (!(tag & mask)) /* Tags out of synch with mask. */
{
warning (_("ERROR RMT: threadinfo tag mismatch."));
retval = 0;
break;
}
if (tag == TAG_THREADID)
{
if (length != 16)
{
warning (_("ERROR RMT: length of threadid is not 16."));
retval = 0;
break;
}
pkt = unpack_threadid (pkt, &ref);
mask = mask & ~TAG_THREADID;
continue;
}
if (tag == TAG_EXISTS)
{
info->active = stub_unpack_int (pkt, length);
pkt += length;
mask = mask & ~(TAG_EXISTS);
if (length > 8)
{
warning (_("ERROR RMT: 'exists' length too long."));
retval = 0;
break;
}
continue;
}
if (tag == TAG_THREADNAME)
{
pkt = unpack_string (pkt, &info->shortname[0], length);
mask = mask & ~TAG_THREADNAME;
continue;
}
if (tag == TAG_DISPLAY)
{
pkt = unpack_string (pkt, &info->display[0], length);
mask = mask & ~TAG_DISPLAY;
continue;
}
if (tag == TAG_MOREDISPLAY)
{
pkt = unpack_string (pkt, &info->more_display[0], length);
mask = mask & ~TAG_MOREDISPLAY;
continue;
}
warning (_("ERROR RMT: unknown thread info tag."));
break; /* Not a tag we know about. */
}
return retval;
}

まず

pkt = unpack_int (pkt, &mask); /* arg mask */
pkt = unpack_threadid (pkt, &ref);

という部分で,16進数の先頭8桁を mask,その後の16桁をスレッドIDとして解析している.その後

if (!threadmatch (&ref, expectedref))

のようにしてスレッドIDをチェックしているので,スレッドIDにはスタブ側に渡された値をそのまま返さないといけないようだ.

次に,

/* Packets are terminated with nulls. */
while ((pkt < limit) && mask && *pkt)
{

のようにして,コマンドの内容を順次解析していく.続くコマンドの内容は

pkt = unpack_int (pkt, &tag); /* tag */
pkt = unpack_byte (pkt, &length); /* length */
if (!(tag & mask)) /* Tags out of synch with mask. */
{
warning (_("ERROR RMT: threadinfo tag mismatch."));
retval = 0;
break;
}

のようにして,まず16進で8桁を tag,続く2桁(1バイトの値)を length として取得している.さらに

if (tag == TAG_THREADID)
{
...

のようにして,tag に応じた値を順次取得していく.まあ解説が面倒なので結論から説明してしまうと,gdb側では欲しい情報を「qP」発行時に mode としてビットマスクで渡し,スタブ側では要求された情報をコマンドに順次詰めて返す(この際に,tag, length, パラメータの順に格納する)ようだ.取得できる情報には,以下の5つがある.
  • TAG_THREADID
  • TAG_EXISTS
  • TAG_THREADNAME
  • TAG_MOREDISPLAY
  • TAG_DISPLAY
まあパラメータのフォーマットに関しては,remote_unpack_thread_info_response() 内の各タグの解析部分を参照してほしい.

で,実装したのがこんな感じ.今回の差分は以下.

diff -ruN kozos20/i386-stub.c kozos21/i386-stub.c
--- kozos20/i386-stub.c Sat Nov 24 09:58:09 2007
+++ kozos21/i386-stub.c Sat Nov 24 11:35:14 2007
@@ -1015,6 +1015,69 @@
ptr = intNToHex(ptr, (int)gen_thread->id, 4);
}
break;
+ case 'P':
+ {
+ int mode;
+ unsigned int threadid[2];
+ kz_thread *thp;
+
+#define TAG_THREADID 1
+#define TAG_EXISTS 2
+#define TAG_DISPLAY 4
+#define TAG_THREADNAME 8
+#define TAG_MOREDISPLAY 16
+ mode = hexToIntN(&ptr, 4);
+ threadid[0] = hexToIntN(&ptr, 4);
+ threadid[1] = hexToIntN(&ptr, 4);
+ thp = (kz_thread *)threadid[1];
+
+ ptr = remcomOutBuffer;
+ *ptr++ = 'Q';
+ *ptr++ = 'P';
+ ptr = intNToHex(ptr, mode, 4);
+ ptr = intNToHex(ptr, threadid[0], 4);
+ ptr = intNToHex(ptr, threadid[1], 4);
+
+ 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);
+ }
+ if (mode & TAG_EXISTS) {
+ ptr = intNToHex(ptr, TAG_EXISTS, 4); /* mode */
+ ptr = intNToHex(ptr, 1, 1); /* length */
+ *ptr++ = '1';
+ }
+ if (mode & TAG_DISPLAY) {
+ ptr = intNToHex(ptr, TAG_DISPLAY, 4); /* mode */
+ ptr = intNToHex(ptr, 3, 1); /* length */
+ {
+ kz_thread *thp2;
+ strcpy(ptr, "SLP");
+ for (thp2 = readyque[thp->pri]; thp2; thp2 = thp2->next) {
+ if (thp == thp2) {
+ strcpy(ptr, "RUN");
+ break;
+ }
+ }
+ ptr += strlen(ptr);
+ }
+ }
+ if (mode & TAG_THREADNAME) {
+ ptr = intNToHex(ptr, TAG_THREADNAME, 4); /* mode */
+ ptr = intNToHex(ptr, strlen(thp->name), 1); /* length */
+ strcpy(ptr, thp->name);
+ ptr += strlen(thp->name);
+ }
+ if (mode & TAG_MOREDISPLAY) {
+ ptr = intNToHex(ptr, TAG_MOREDISPLAY, 4); /* mode */
+ ptr = intNToHex(ptr, 2, 1); /* length */
+ ptr = intNToHex(ptr, thp->pri, 1);
+ }
+ *ptr = '\0';
+ }
+ break;
default:
break;
}
diff -ruN kozos20/thread.h kozos21/thread.h
--- kozos20/thread.h Sat Nov 24 09:58:09 2007
+++ kozos21/thread.h Sat Nov 24 11:26:13 2007
@@ -40,6 +40,7 @@
} kz_thread;

extern kz_thread threads[THREAD_NUM];
+extern kz_thread *readyque[PRI_NUM];
extern kz_thread *current;
extern sigset_t block;

「qP」に対してスレッド情報を返信している.まあパラメータの細かいフォーマットについては,remote_unpack_thread_info_response()での解析処理を参照してほしい.

remote_threads_extra_info() を見たところ,TAG_DISPLAY はスレッドの状態(スリープ,ランニング),TAG_THREADNAME はスレッド名,TAG_MOREDISPLAY は優先度として表示するようなので,そーいうふうに応答している.内部で strlen() を使ってしまっていて,スタブ内部からライブラリ関数を呼ぶのは実はあまりよくない(第10回参照)のだけど,面倒なのでご愛敬.

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

画像はこちら

おー,スレッド名とかが表示されている.

このときの通信内容は以下.

[$qfThreadInfo#bb](+)($#00)[+]
[$qL1200000000000000000#50](+)($qM010000000000000000000000000080c4480#9a)[+]
[$qL02000000000080c4480#9a](+)($qM01000000000080c448000000000080c4780#e8)[+]
[$qL02000000000080c4780#9d](+)($qM01000000000080c478000000000080c4a80#15)[+]
[$qL02000000000080c4a80#c7](+)($qM01000000000080c4a8000000000080c4d80#42)[+]
[$qL02000000000080c4d80#ca](+)($qM01000000000080c4d8000000000080c5080#12)[+]
[$qL02000000000080c5080#97](+)($qM01000000000080c508000000000080c5380#e2)[+]
[$qL02000000000080c5380#9a](+)($qM01000000000080c538000000000080c5680#e8)[+]
[$qL02000000000080c5680#9d](+)($qM01100000000080c5680#9e)[+]
[$qThreadExtraInfo,80c5680#23](+)($#00)[+]
[$qP0000001f00000000080c5680#c6](+)($QP0000001f00000000080c5680000000011000000000080c5680000000020110000000403SLP0000000805httpd000000100209#1d)[+]
[$Hg80c5680#4d](+)($OK#9a)[+]
[$g#67](+)($0000000000000000ea04000080560c08fcc5110818c61108ecc7110800000000ebed050802020000330000003b0000003b0000003b0000003b0000001b000000#49)[+]
[$qP0000001f00000000080c5380#c3](+)($QP0000001f00000000080c5380000000011000000000080c5380000000020110000000403SLP0000000807telnetd000000100208#e4)[+]
[$Hg80c5380#4a](+)($OK#9a)[+]
[$g#67](+)($0000000000000000ea04000080530c08fc35110818361108ec37110800000000ebed050802020000330000003b0000003b0000003b0000003b0000001b000000#b6)[+]
[$qP0000001f00000000080c5080#c0](+)($QP0000001f00000000080c5080000000011000000000080c5080000000020110000000403SLP0000000805clock000000100207#f7)[+]
[$Hg80c5080#47](+)($OK#9a)[+]
[$g#67](+)($0000000098b00d08ea04000080500c083ca7100858a71008eca7100800000000ebed050806020000330000003b0000003b0000003b0000003b0000001b000000#91)[+]
[$qP0000001f00000000080c4d80#f3](+)($QP0000001f00000000080c4d80000000011000000000080c4d80000000020110000000403RUN0000000804idle00000010021f#24)[+]
[$Hg80c4d80#7a](+)($OK#9a)[+]
[$g#67](+)($0400000004000000ffffffff804d0c088c171008b8171008ec171008000000008f08060813020000330000003b0000003b0000003b0000003b0000001b000000#aa)[+]
[$qP0000001f00000000080c4a80#f0](+)($QP0000001f00000000080c4a80000000011000000000080c4a80000000020110000000403SLP0000000806outlog000000100203#e2)[+]
[$Hg80c4a80#77](+)($OK#9a)[+]
[$g#67](+)($0000000060800f08ea040000804a0c083c870f0858870f08ec870f0800000000ebed050806020000330000003b0000003b0000003b0000003b0000001b000000#b2)[+]
[$qP0000001f00000000080c4780#c6](+)($QP0000001f00000000080c4780000000011000000000080c4780000000020110000000403RUN0000000805stubd000000100202#1a)[+]
[$Hg80c4780#4d](+)($OK#9a)[+]
[$g#67](+)($00880d0800000000ea04000080470c0858f60e0858f60e08ecf70e080000000099ae040802020000330000003b0000003b0000003b0000003b0000001b000000#87)[+]
[$qP0000001f00000000080c4480#c3](+)($QP0000001f00000000080c4480000000011000000000080c4480000000020110000000403SLP0000000807extintr000000100201#fb)[+]
[$Hg80c4480#4a](+)($OK#9a)[+]
[$g#67](+)($0000000006000000ea04000080440c086c660e0888660e08ec670e0800000000ebed050806020000330000003b0000003b0000003b0000003b0000001b000000#3a)[+]
[$Hg80c4780#4d](+)($OK#9a)[+]
[$g#67](+)($00880d0800000000ea04000080470c0858f60e0858f60e08ecf70e080000000099ae040802020000330000003b0000003b0000003b0000003b0000001b000000#87)[+]
(この状態で停止)

「qP」に対して「QP」でスレッド情報が返されていることに注目.問題なく動作しているようだ.

しかしここでもう一度 info threads を行うと,なんか固まってしまうようだ.

画像はこちら

このときの通信内容は以下.

[$T080c5680#22](+)($#00)[+]
[$T080c5380#1f](+)($#00)[+]
[$T080c5080#1c](+)($#00)[+]
[$T080c4d80#4f](+)($#00)[+]
[$T080c4a80#4c](+)($#00)[+]
[$T080c4780#22](+)($#00)[+]
[$T080c4480#1f](+)($#00)[+]
[$qL120f0000000080c5680#d4](+)($qM011f0000000080c5680#d4)[+]
[$qP0000001f00000000ffffffff#28](+)
(この状態で停止)

どうも「qP」コマンドでスレッドIDが 0xffffffff として渡されているため,スレッドの検索に失敗しているようだ.

うーん,スレッドIDが 0xffffffff ってどういう意味なのだろう?カレントスレッドの情報を返せばいいような気もするが...まあこのへんはまた次回考えよう.
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

今回も前々回,前回に引続き,スレッド対応を進めよう.とりあえずコマンドが足りないことが明らかにわかっているので,それらを追加してみる.

まずは前回の通信結果を見てみよう.

[$qfThreadInfo#bb](+)($#00)[+]
[$qL1200000000000000000#50](+)($qM010000000000000000000000000080c3360#96)[+]
[$qL02000000000080c3360#96](+)($qM01000000000080c336000000000080c3660#e0)[+]
[$qL02000000000080c3660#99](+)($qM01000000000080c366000000000080c3960#e6)[+]
[$qL02000000000080c3960#9c](+)($qM01000000000080c396000000000080c3c60#13)[+]
[$qL02000000000080c3c60#c6](+)($qM01000000000080c3c6000000000080c3f60#40)[+]
[$qL02000000000080c3f60#c9](+)($qM01000000000080c3f6000000000080c4260#10)[+]
[$qL02000000000080c4260#96](+)($qM01000000000080c426000000000080c4560#e0)[+]
[$qL02000000000080c4560#99](+)($qM01100000000080c4560#9a)[+]
[$qC#b4](+)($#00)[+]
[$qThreadExtraInfo,80c4560#1f](+)($#00)[+]
[$qP0000001f00000000080c4560#c2](+)($#00)[+]
[$Hg80c4560#49](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$qP0000001f00000000080c4260#bf](+)($#00)[+]
[$Hg80c4260#46](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$qP0000001f00000000080c3f60#f2](+)($#00)[+]
[$Hg80c3f60#79](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$qP0000001f00000000080c3c60#ef](+)($#00)[+]
[$Hg80c3c60#76](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$qP0000001f00000000080c3960#c5](+)($#00)[+]
[$Hg80c3960#4c](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$qP0000001f00000000080c3660#c2](+)($#00)[+]
[$Hg80c3660#49](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$qP0000001f00000000080c3360#bf](+)($#00)[+]
[$Hg80c3360#46](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$Hg0#df](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
(この状態で停止)

で,スタブ側で応答できていないコマンドをリストアップすると,以下になる.
  • qfThreadInfo
  • qC
  • qThreadExtraInfo,80c4560
  • qP0000001f00000000080c4560
  • Hg80c4560
これらはどれもスタブ側では対応されていないため,$#00を返している.で,なにを返さなければいけないかというと,資料もサンプルも無いので,いつもどおりremote.cを読んで調べるしかない.

まず手始めに qfThreadInfo だが,これは前々回に説明したように,実装の必要は無い.

次に qC だが,これは

...
[$qL02000000000080c4260#96](+)($qM01000000000080c426000000000080c4560#e0)[+]
[$qL02000000000080c4560#99](+)($qM01100000000080c4560#9a)[+]
[$qC#b4](+)($#00)[+]
...

のようにして,qLコマンドによるスレッド一覧取得のあとに発行されている.ということは,remote_threads_info()からの一連のスレッド情報取得処理のあとに,qCコマンドを発行する部分があるのではなかろうか?

ほんとはこーいうときこそ,デバッガのステップ実行で処理の流れを見ていくのが便利なのだが,まあソースコードをよく読んでみると,remote_threads_info() の最後で remote_find_new_threads() という関数を呼んでおり,さらにそこから呼んでいる remote_current_thread() という関数の内部で,qC コマンドを発行しているようだ.(ていうかそれよりもまずはためしに「qC」で検索かけるべきだったね.そしたら一撃で見つかった)

static ptid_t
remote_current_thread (ptid_t oldpid)
{
struct remote_state *rs = get_remote_state ();

putpkt ("qC");
getpkt (&rs->buf, &rs->buf_size, 0);
if (rs->buf[0] == 'Q' && rs->buf[1] == 'C')
/* Use strtoul here, so we'll correctly parse values whose highest
bit is set. The protocol carries them as a simple series of
hex digits; in the absence of a sign, strtol will see such
values as positive numbers out of range for signed 'long', and
return LONG_MAX to indicate an overflow. */
return pid_to_ptid (strtoul (&rs->buf[2], NULL, 16));
else
return oldpid;
}

qCコマンドの発行時には,応答として QC コマンドというのが返るようだ.で,引数になんか整数がつくらしい.まあ関数の名前が remote_current_thread() なので,カレントスレッドのスレッドIDを返せばいいように思える.

ここで返したスレッドIDは,呼び出し元の remote_find_new_threads() で

static void
remote_find_new_threads (void)
{
remote_threadlist_iterator (remote_newthread_step, 0,
CRAZY_MAX_THREADS);
if (PIDGET (inferior_ptid) == MAGIC_NULL_PID) /* ack ack ack */
inferior_ptid = remote_current_thread (inferior_ptid);
}

のようにして,inferior_ptid という変数に代入される.で,remote_fetch_registers() で

static void
remote_fetch_registers (struct regcache *regcache, int regnum)
{
struct remote_state *rs = get_remote_state ();
struct remote_arch_state *rsa = get_remote_arch_state ();
int i;

set_thread (PIDGET (inferior_ptid), 1);
...

のようにして,set_thread() により general_thread という変数に設定される.set_thread() の内部では general_thread と continue_thread という2種類の変数を設定しており,どうも gdb は General Thread と Continue Thread という2種類のカレントスレッドを持っているように思える.想像だが,General Thread は現在デバッグ対象となっているスレッド,Continue Thread は continue 実行時に動作再開すべきスレッド?(つまり,現在ブレークしているスレッド?)のような気がする...のだが,詳細未調査.

まあとりえあずは,qCにはカレントスレッドを返せばいいように思えるのでそうする.

で,次は qThreadExtraInfo,80c4560 というコマンドだ.これはremote_threads_extra_info() という関数で発行している(「ThreadExtraInfo」で検索したら一撃で見つかった).で,何を期待しているのか remote_threads_extra_info() を見てみたのだが,どうもスレッドの付加情報(名前とか優先度とか)を取得するためのコマンドらしく,とりあえずは無くても動くみたいだ.なので今回は未実装とする.

次は qP0000001f00000000080c4560 というコマンドだ.これは「qP」で検索したが...出てこない...「'P'」で検索したら,以下が出てきた.

static char *
pack_threadinfo_request (char *pkt, int mode, threadref *id)
{
*pkt++ = 'q'; /* Info Query */
*pkt++ = 'P'; /* process or thread info */
pkt = pack_int (pkt, mode); /* mode */
pkt = pack_threadid (pkt, id); /* threadid */
*pkt = '\0'; /* terminate */
return pkt;
}

引数として整数値をひとつ,スレッドIDをひとつ送ってくるようだ.で,この pack_threadinfo_request() の呼び出しもとなのだけど,remote_get_threadinfo()を経由して2箇所から呼ばれている.ひとつは,先程 qThreadExtraInfo コマンドを呼び出していたremote_threads_extra_info() の内部で,qThreadExtraInfo に失敗した際に

if (remote_get_threadinfo (&id, set, &threadinfo))
if (threadinfo.active)
{
if (*threadinfo.shortname)
n += xsnprintf (&display_buf[0], sizeof (display_buf) - n,
" Name: %s,", threadinfo.shortname);
if (*threadinfo.display)
n += xsnprintf (&display_buf[n], sizeof (display_buf) - n,
" State: %s,", threadinfo.display);
...

のようにして remote_get_threadinfo() を呼び出している.応答を受信した際には,スレッド名とかの設定が行われるように見える.

もう1箇所は,get_and_display_threadinfo()という関数から呼ばれている.

int
get_and_display_threadinfo (threadref *ref)
{
int result;
int set;
struct gdb_ext_thread_info threadinfo;

set = TAG_THREADID | TAG_EXISTS | TAG_THREADNAME
| TAG_MOREDISPLAY | TAG_DISPLAY;
if (0 != (result = remote_get_threadinfo (ref, set, &threadinfo)))
display_thread_info (&threadinfo);
return result;
}

で,qPを発行するタイミングなのだけど,前者のほうでは qThreadExtraInfo 失敗時にqP コマンドが発行されているので,どうも qfThreadInfo に対する qL コマンドのように
  • qThreadExtraInfo ... 先進的で高効率な,スレッド情報取得方法
  • qP ... レガシーな,スレッド情報取得方法
という関係にあるのではなかろうか.gdb側ではまずは qThreadExtraInfo を試してみて,ダメならば qP を試してみるわけだ.まあ実際のところ remote_threads_extra_info() の内部ではスレッド名などの設定をしているので,関数名の通り,スレッドの拡張情報取得コマンドだと思われる.なので qThreadExtraInfo と同様,とりあえずは未実装でいいだろう.

次は Hg80c4560 だ.これは「'H'」で検索したら,以下のような部分があった.

static void
set_thread (int th, int gen)
{
struct remote_state *rs = get_remote_state ();
char *buf = rs->buf;
int state = gen ? general_thread : continue_thread;

if (state == th)
return;

buf[0] = 'H';
buf[1] = gen ? 'g' : 'c';
if (th == MAGIC_NULL_PID)
{
buf[2] = '0';
buf[3] = '\0';
}
else if (th < 0)
xsnprintf (&buf[2], get_remote_packet_size () - 2, "-%x", -th);
else
xsnprintf (&buf[2], get_remote_packet_size () - 2, "%x", th);
putpkt (buf);
getpkt (&rs->buf, &rs->buf_size, 0);
if (gen)
general_thread = th;
else
continue_thread = th;
}

引数としてはスレッドIDを渡してくるようだ.応答はとくに見ていないので,スタブに対するなんらかの指示をしているように思われる.スタブ側に送ったスレッドIDを general_thread (もしくは continue_thread)に設定しているので,カレントスレッドの切替えの指示だと思われる...と,ここまで書いて思い出したけど,考えてみれば前々回の最後で,Hgはカレントスレッドの切替えだろうと書いていたね...ということで,スレッドIDで指定されたスレッドに,カレントスレッドを切替えればいいわけだ.

で,書いたのがこんな感じ.差分は以下.今回も i386-stub.c のみの修正.

diff -ruN kozos19/i386-stub.c kozos20/i386-stub.c
--- kozos19/i386-stub.c Mon Nov 19 23:37:48 2007
+++ kozos20/i386-stub.c Tue Nov 20 00:49:16 2007
@@ -92,6 +92,7 @@
#include
#include
#include "thread.h"
+#include "stublib.h"

/************************************************************************
*
@@ -1006,10 +1007,45 @@
*ptr++ = '\0';
}
break;
+ case 'C':
+ {
+ ptr = remcomOutBuffer;
+ *ptr++ = 'Q';
+ *ptr++ = 'C';
+ ptr = intNToHex(ptr, (int)gen_thread->id, 4);
+ }
+ break;
default:
break;
}
break;
+
+ case 'H':
+ switch (*ptr++)
+ {
+ case 'g':
+ {
+ int val, rev = 0;
+ if (*ptr == '-')
+ {
+ rev++;
+ ptr++;
+ }
+ if (hexToInt(&ptr, &val))
+ {
+ if (rev) val = -val;
+ stub_restore_regs(gen_thread);
+ gen_thread = (kz_thread *)val;
+ stub_store_regs(gen_thread);
+ strcpy (remcomOutBuffer, "OK");
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ break;
+
} /* switch */

/* reply to the request */

まあすでに説明してしまったが,qC コマンドと Hg コマンドを実装してある.qC はカレントスレッドを返し,Hg はカレントスレッドの切替えを行っている.注意として set_thread() の内部ではスレッドIDにマイナスを付加して送信する場合があるので,Hg受信時には '-' の有無を見ている.スレッドの切替えは,stublib.c で提供されているstub_restore_regs(), stub_store_regs() を利用している.stub_store_regs() を呼び出すと,レジスタの値を格納している配列 registers[] が指定されたスレッドのものに書きかわる.そして g コマンド受信時には,配列 registers[] が参照されてレジスタ値をgdbに送信するので,gdb側ではカレントスレッドのレジスタ値を受け取ることになる.

ちなみに gen_thread は stublib.c で定義されている外部変数で,スタブのカレントスレッドである.(thread.cが持っているカレントスレッド(変数current)ではないので注意.current はKOZOSのカレントスレッドだが,gen_thread はスタブで現在デバッグ対象になっているスレッドである)

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

画像はこちら

おー,こんどはなんかスレッドごとにそれっぽい情報が表示されている.ちなみに左端に * がついているのが,現在デバッグ対象となっているカレントスレッドだ.breakpoint() で停止していうので,おそらくこれがstubd だろう(Ctrl-C受信により,kz_break()を呼び出して停止している).

他のスレッドは,だいたい kill() で止まっているようだ.これはシステムコール呼び出しにより,kz_syscall()内の kill(..., SIGSYS) で停止しているからだ.たぶん kz_recv() によるメッセージ受信待ちになっているのだと思われる.ひとつだけ select() で停止しているスレッドがいるが,これは idle スレッドだ.

info threads 実行時の通信内容は以下になる.

[$qfThreadInfo#bb](+)($#00)[+]
[$qL1200000000000000000#50](+)($qM010000000000000000000000000080c3360#96)[+]
[$qL02000000000080c3360#96](+)($qM01000000000080c336000000000080c3660#e0)[+]
[$qL02000000000080c3660#99](+)($qM01000000000080c366000000000080c3960#e6)[+]
[$qL02000000000080c3960#9c](+)($qM01000000000080c396000000000080c3c60#13)[+]
[$qL02000000000080c3c60#c6](+)($qM01000000000080c3c6000000000080c3f60#40)[+]
[$qL02000000000080c3f60#c9](+)($qM01000000000080c3f6000000000080c4260#10)[+]
[$qL02000000000080c4260#96](+)($qM01000000000080c426000000000080c4560#e0)[+]
[$qL02000000000080c4560#99](+)($qM01100000000080c4560#9a)[+]
[$qThreadExtraInfo,80c4560#1f](+)($#00)[+]
[$qP0000001f00000000080c4560#c2](+)($#00)[+]
[$Hg80c4560#49](+)($OK#9a)[+]
[$g#67](+)($0000000000000000e203000060450c08fcb5110818b61108ecb711080000000067eb050802020000330000003b0000003b0000003b0000003b0000001b000000#b6)[+]
[$qP0000001f00000000080c4260#bf](+)($#00)[+]
[$Hg80c4260#46](+)($OK#9a)[+]
[$g#67](+)($0000000000000000e203000060420c08fc25110818261108ec2711080000000067eb050802020000330000003b0000003b0000003b0000003b0000001b000000#23)[+]
[$qP0000001f00000000080c3f60#f2](+)($#00)[+]
[$Hg80c3f60#79](+)($OK#9a)[+]
[$g#67](+)($0000000098a00d08e2030000603f0c083c97100858971008ec9710080000000067eb050806020000330000003b0000003b0000003b0000003b0000001b000000#be)[+]
[$qP0000001f00000000080c3c60#ef](+)($#00)[+]
[$Hg80c3c60#76](+)($OK#9a)[+]
[$g#67](+)($0400000004000000ffffffff603c0c088c071008b8071008ec071008000000000b06060813020000330000003b0000003b0000003b0000003b0000001b000000#95)[+]
[$qP0000001f00000000080c3960#c5](+)($#00)[+]
[$Hg80c3960#4c](+)($OK#9a)[+]
[$g#67](+)($0000000060700f08e203000060390c083c770f0858770f08ec770f080000000067eb050806020000330000003b0000003b0000003b0000003b0000001b000000#f7)[+]
[$qP0000001f00000000080c3660#c2](+)($#00)[+]
[$Hg80c3660#49](+)($OK#9a)[+]
[$g#67](+)($00780d0800000000e203000060360c0858e60e0858e60e08ece70e080000000015ac040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$qP0000001f00000000080c3360#bf](+)($#00)[+]
[$Hg80c3360#46](+)($OK#9a)[+]
[$g#67](+)($0000000006000000e203000060330c086c560e0888560e08ec570e080000000067eb050806020000330000003b0000003b0000003b0000003b0000001b000000#a7)[+]
[$Hg80c3660#49](+)($OK#9a)[+]
[$g#67](+)($00780d0800000000e203000060360c0858e60e0858e60e08ece70e080000000015ac040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
(この状態で停止)

まあもうさんざん説明しているので細かい説明は省くが,HgコマンドとqCコマンドが応答している...と思ったのだが,よく見るとqCが来ていないね.これは実は,info threads 実行前にすでにqCが発行されていて,それが成功しているので,改めて発行されていないということのようだ.(実際に確認してみたら,info threads 実行の前に qC があって,ちゃんと値が返っていた.このように,新しいコマンドが実装されると,コマンドの呼び出しシーケンスが微妙に変わったりするので注意)

とりあえず,ちゃんと動いた.ただ,せめてスレッド名くらいは出てくれないと,情報としてちょっと寂しいよね.まあこのへんはまた次回考えよう.
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

今回はスタブのエンディアンについて考えよう.

まずエンディアンについてだけど,前回の結果では,スレッドIDのエンディアンがどうもひっくり返っているらしいという問題があった.

エンディアンに対する考え方は,3種類あるとおもう.
  1. ターゲット機種に依存したエンディアンにする.(たとえばターゲット機種がi386ならばリトルエンディアンで通信するが,MIPSとかならビッグエンディアンで通信する)
  2. ホストPCの機種に依存したエンディアンにする.
  3. 通信はビッグエンディアンかリトルエンディアンのどちらかに固定する.
まず1だが,スタブ側としては一番実装しやすい.というのは,あまりエンディアンのことを考える必要が無くなるからだ.しかしgdb側では,ターゲット機種に応じた動作をする必要がある.

次に2だが,これは問題だ.スタブ側ではホストPCの機種など(gdb側から通知しない限りは)知るよしもない.なのにホストPC上のgdbが自分のエンディアンで動いてしまうと,たとえばAT互換機上の Linux で動かす場合にはリトルエンディアンだが,PowerMAC上の Linux で動かす場合にはビッグエンディアン,ということになってしまう.それに応じてスタブ側ではうまくエンディアンを切替えるか,もしくはホストPCをAT互換機とかに固定してしまうか,なんというかいまいちな動作になってしまう.

次に3だが,ターゲット依存もなくなるので,gdb側としてはこれがいちばんうれしい.通常ネットワーク通信はビッグエンディアンなので,ビッグエンディアンに固定するのがいいように思う.

で,実際にgdbがどのように動作しているのかなのだが,まあエンディアンについては前回紹介したremote.cを良く読むか,スタブのサンプル(場合によっては他CPUの)を見て,コマンドごとに判断するしか無い.たとえば remote.c では,整数値のデコーディングは以下のようになっている.

static int
stub_unpack_int (char *buff, int fieldlength)
{
int nibble;
int retval = 0;

while (fieldlength)
{
nibble = stubhex (*buff++);
retval |= nibble;
fieldlength--;
if (fieldlength)
retval = retval << 4;
}
return retval;
}

これは,先頭の桁を4ビットずつシフトしながら加算しているので,ホストPCのエンディアンに依存せず,必ずビッグエンディアンとして値を読む.

しかしスレッドIDのデコーディングは以下のようになっている.

static char *
unpack_threadid (char *inbuf, threadref *id)
{
char *altref;
char *limit = inbuf + BUF_THREAD_ID_SIZE;
int x, y;

altref = (char *) id;

while (inbuf < limit)
{
x = stubhex (*inbuf++);
y = stubhex (*inbuf++);
*altref++ = (x << 4) | y;
}
return inbuf;
}

これは,引数idで渡されたアドレスに先頭桁から順次格納していくので,ホストPCのエンディアン依存になる.げげっ!

...ように思えるのだが,よく読むと実はこのあとに呼ばれるremote_newthread_step() の内部の処理で threadref_to_int() を通すことで,スレッドIDをビッグエンディアンとして読み直している.あー,あせった.ちなみに threadref_to_int() は以下.

static int
threadref_to_int (threadref *ref)
{
int i, value = 0;
unsigned char *scan;

scan = *ref;
scan += 4;
i = 4;
while (i-- > 0)
value = (value << 8) | ((*scan++) & 0xff);
return value;
}

つまり,remote.c 内部では整数値はビッグエンディアンとして読み込んでいるのだが,スレッドIDに関しては,とりあえず送られてきたままのバイト列をスレッドIDとしてそのまま格納し,あとで threadref_to_int() によってビッグエンディアンとして読み,整数値に変換している,ということになる.注意しなければならないのは,qLコマンドによるnext threadID の送信だ.これは整数値への変換前の(remote_threadlist_iterator()内部でcopy_threadref()によってコピーした)バイト列をそのまま送ってくる.このように remote.c 内部では,スレッドIDをバイト列のまま扱っているので注意が必要だ.

まあ結局のところ,スタブ側では整数値もスレッドIDも,ビッグエンディアンとして処理すればいいようだ.ちなみに以下は i386-stub.c のメモリ→通信用文字列への変換関数である.

char *
mem2hex (mem, buf, count, may_fault)
char *mem;
char *buf;
int count;
int may_fault;
{
int i;
unsigned char ch;

if (may_fault)
mem_fault_routine = set_mem_err;
for (i = 0; i < count; i++)
{
ch = get_char (mem++);
if (may_fault && mem_err)
return (buf);
*buf++ = hexchars[ch >> 4];
*buf++ = hexchars[ch % 16];
}
*buf = 0;
if (may_fault)
mem_fault_routine = NULL;
return (buf);
}

まあよく読めばわかるのだけど,メモリ上の値を先頭バイトから16進数の文字列に変換するだけだ.前回の qL コマンド対応では,この mem2hex() を使って通信データを作成していた.なのでターゲット機種(この場合は,実行形式kozが動作しているPC)である i386 のエンディアンになってしまい,リトルエンディアンとしてスレッドIDが送信されてしまうのが前回のエンディアンの問題の原因だ.

ここでちょっと気がつくことがある.i386-stub.c 内部では,g コマンドによるレジスタ値送信や m コマンドによるメモリ値送信にもmem2hex() が使われているのだ.まあもっとも m コマンドの場合には,メモリ上の値をバイト列として返すので,そもそもエンディアンを考えるべきではなく,単に先頭から順に返せばよい.よってmem2hex() で差し支えない.気になるのは g コマンドによるレジスタ値取得なのだが,レジスタはターゲット機種固有のものなので,ターゲット機種のエンディアンで返すべき,という考えなのだろう,多分.(そもそもレジスタが4バイトだという保証も無いわけだし.スレッドIDなどはCPUに依存する値ではないので,ビッグエンディアンに統一すべきだといえる)

ということでスタブ側では,スレッドIDの送受信部分はビッグエンディアン使用に書き換える必要がある.で,修正したのが以下.で,以下が今回の修正ぶんだ.ちなみに今回は,修正はスタブのみ.

diff -ruN kozos18/i386-stub.c kozos19/i386-stub.c
--- kozos18/i386-stub.c Mon Nov 19 18:42:25 2007
+++ kozos19/i386-stub.c Mon Nov 19 21:53:46 2007
@@ -752,6 +752,41 @@
return (numChars);
}

+int
+hexToIntN (char **ptr, int num)
+{
+ int i;
+ int intValue = 0, hexValue;
+
+ for (i = 0; i < num * 2; i++)
+ {
+ hexValue = hex (**ptr);
+ if (hexValue >= 0)
+ intValue = (intValue << 4) | hexValue;
+ else
+ break;
+
+ (*ptr)++;
+ }
+
+ return (intValue);
+}
+
+char *
+intNToHex (char *ptr, int intValue, int num)
+{
+ int hexValue;
+
+ for (; num; num--)
+ {
+ hexValue = (intValue >> ((num - 1) * 8)) & 0xff;
+ *ptr++ = hexchars[hexValue >> 4];
+ *ptr++ = hexchars[hexValue & 0xf];
+ }
+ *ptr = '\0';
+ return (ptr);
+}
+
/*
* This function does all command procesing for interfacing to gdb.
*/
@@ -939,7 +974,8 @@
countmax = hex(*ptr++) << 4;
countmax += hex(*ptr++);

- hex2mem(ptr, threadid, 8, 0);
+ threadid[0] = hexToIntN(&ptr, 4);
+ threadid[1] = hexToIntN(&ptr, 4);

doneflag = 1;
for (i = 0; i < THREAD_NUM; i++) {
@@ -958,12 +994,14 @@
*ptr++ = hexchars[count >> 4];
*ptr++ = hexchars[count & 0xf];
*ptr++ = doneflag ? '1' : '0';
- ptr = mem2hex(threadid, ptr, 8, 0);
+ ptr = intNToHex(ptr, threadid[0], 4);
+ ptr = intNToHex(ptr, threadid[1], 4);

if (!doneflag) {
threadid[0] = 0;
threadid[1] = (int)thp->id;
- ptr = mem2hex(threadid, ptr, 8, 0);
+ ptr = intNToHex(ptr, threadid[0], 4);
+ ptr = intNToHex(ptr, threadid[1], 4);
}
*ptr++ = '\0';
}

まずスタブでの受信時には,16進文字列→整数値への変換が必要だ.これは前回は hex2mem() という関数を使用したが,これを使ったためにスタブ依存のエンディアンで読んでしまっていた.基本として mem2hex() や hex2mem() は,メモリ上の生の値をバイト単位で送受信するためのサービス関数なので,今回のような整数値の送受信などに利用すべきではない.受信には,文字列をビッグエンディアンとして扱う hexToInt() という関数がすでに用意されているので,こちらを利用しよう...と思ったのだが,hexToInt()は16進文字列がそれ以外の文字で終端されていることを期待しているので,今回のスレッドIDのように,8バイトの整数値(int型2個ぶん)が続けて送られてくるような場合には利用できない.なので,任意サイズの固定長を扱えるhexToIntN() という関数を新規に作成し,そちらを使うようにしている.

あーあと言うの忘れていたけど,gdbではスレッドIDは8バイト(64ビット)として管理しているようだ(64ビットCPUへの考慮か?).KOZOSではスレッドIDは4バイトなので,通信の際には64ビットの半分だけを使用して,残り半分はゼロで埋めている.

次に送信だが,残念ながら整数値をビッグエンディアンとして送信するようなサービス関数がスタブ上に用意されていない.なので,hexToIntN() に対してintNToHex() という関数を作成し,利用している.

では,実行してみよう.前回と同様に実行形式を起動,gdbで接続,continue,Ctrl-Cブレークして,info threads を実行してみる.

画像はこちら

スレッド情報として,

1 Thread 135017312 breakpoint () at i386-stub.c:1059

などのように表示されている.この 135017312 という値は,16進数になおすと0x080c3360 となりそれっぽい値になるので,前回とは異なりスレッドIDが正常にやりとりできていることがわかる.

info threads 実行時の通信内容は以下の通り.

[$qfThreadInfo#bb](+)($#00)[+]
[$qL1200000000000000000#50](+)($qM010000000000000000000000000080c3360#96)[+]
[$qL02000000000080c3360#96](+)($qM01000000000080c336000000000080c3660#e0)[+]
[$qL02000000000080c3660#99](+)($qM01000000000080c366000000000080c3960#e6)[+]
[$qL02000000000080c3960#9c](+)($qM01000000000080c396000000000080c3c60#13)[+]
[$qL02000000000080c3c60#c6](+)($qM01000000000080c3c6000000000080c3f60#40)[+]
[$qL02000000000080c3f60#c9](+)($qM01000000000080c3f6000000000080c4260#10)[+]
[$qL02000000000080c4260#96](+)($qM01000000000080c426000000000080c4560#e0)[+]
[$qL02000000000080c4560#99](+)($qM01100000000080c4560#9a)[+]
[$qC#b4](+)($#00)[+]
[$qThreadExtraInfo,80c4560#1f](+)($#00)[+]
[$qP0000001f00000000080c4560#c2](+)($#00)[+]
[$Hg80c4560#49](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$qP0000001f00000000080c4260#bf](+)($#00)[+]
[$Hg80c4260#46](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$qP0000001f00000000080c3f60#f2](+)($#00)[+]
[$Hg80c3f60#79](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$qP0000001f00000000080c3c60#ef](+)($#00)[+]
[$Hg80c3c60#76](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$qP0000001f00000000080c3960#c5](+)($#00)[+]
[$Hg80c3960#4c](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$qP0000001f00000000080c3660#c2](+)($#00)[+]
[$Hg80c3660#49](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$qP0000001f00000000080c3360#bf](+)($#00)[+]
[$Hg80c3360#46](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
[$Hg0#df](+)($#00)[+]
[$g#67](+)($00780d08000000001504000060360c0858e60e0858e60e08ece70e08000000003dab040802020000330000003b0000003b0000003b0000003b0000001b000000#41)[+]
(この状態で停止)

まあやはり qC とか Hg とかいったコマンドがスタブで処理できていないのだが,このへんはまた次回!
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

本日本屋に行って初めて気がついたのだけど,Interface誌の今月号で「組み込みクロス開発環境構築テクニック」という特集をやっていて,GDBスタブの実装の記事がある.とてもよくまとまった内容で,再確認するというか,なるほど,こーいうことだったんだーというようなことがけっこうあった.必読.

あーあと今日で連載開始してちょうど1ヵ月だね.ここまででけっこういろいろと解説してきたけどどうだろう.まああまり丁寧な校正をしないで思い付くままに書いているだけなので日本語がヘンだったり説明不足な部分もあるだろうが,ご勘弁.ネタはまだまだあるので,まだまだ続けてみたいとは思っている.

さて今回だが,KOZOSもGDBスタブもだいたいいい感じで動作するようになってきている.ていうか基本機能はほとんど動いている.なので,GDBスタブの追加機能(スタブのオプション機能)としてスレッド対応をしてみたい.

GDBには,以下のスレッド管理機能がある.このへんについてはGDBのマニュアル本とかにも説明があるのでそちらを参照.
  • シグナル発生時のスレッド表示
  • info threads コマンドによる,スレッド一覧の取得
  • thread コマンドによる,カレントスレッドの切替え
  • ブレークポイントのスレッド単位での指定
これらの実現にはGDBスタブへのオプションのコマンドの実装が必要になるのだが,これは僕の調査不足かもしれないけど,どーいう機能をどーいうふうに実装すればいいのか,きちんとした資料が見当たらない.

ということで,GDBプロトコルを読む,あとはGDBのソースコードを読んでてきとうに実装してみる.まあこーいうように資料が無くても自分で調べてなんとかすることこそ,技術者の腕の見せどころだ!

で,さっそくGDBプロトコルを見てみる.第16回で使用したKOZOSのソースコードで実行形式を作成し,起動,GDBで接続する.

Sun Nov 18 21:40:25 2007
Sun Nov 18 21:40:26 2007
Sun Nov 18 21:40:27 2007
Sun Nov 18 21:40:28 2007
Sun Nov 18 21:40:29 2007
...

時刻表示が起動している.さらに Ctrl-C で停止.

Sun Nov 18 21:40:47 2007
Sun Nov 18 21:40:48 2007
Sun Nov 18 21:40:49 2007
($T054:58e60e08;5:58e60e08;8:c5a80408;#c0)[+]
[$M8048088,1:55#c2](+)($OK#9a)[+]
(この状態で停止)

で,まずはこの状態でスレッド一覧を取得するために,info threads コマンドを実行してみよう.

画像はこちら

エラーとなり,スレッド情報を取得できなかった.まあスレッドはKOZOSが管理しているものなのでOS依存になるのが自明の理なので,スタブ側にスレッド対応がしていないのでこれは当然のことだ.

この状態で,gdbとスタブ間では以下の通信が行われていた.

[$qfThreadInfo#bb](+)($#00)[+]
[$qL1200000000000000000#50](+)($#00)[+]
[$qC#b4](+)($#00)[+]
(この状態で停止)

まず qfThreadInfo というコマンドが発行されているが,未実装なので $#00 を返し,次に qL というコマンドを発行しているが,これも未実装なので $#00 を返し,さらに qC というコマンドを発行しているが,これも未実装なので $#00 を返している.結果としてスレッド情報が得られないので,gdbではエラーとなっているのだろう.

なので,スタブ側でこれらのコマンドに対応し,スレッド情報を返すようにすればいいことになる.で,問題は何を返せばいいのかなのだが,資料が無い.なので gdb のソースを直接読んで考えてみる.なお今回使用するgdbのソースコードはgdb-6.7 のものである.というか,この連載で一貫して使用しているのはgdb-6.7である.書くのを忘れていた.

gdb のリモートデバッグ関連のソースコードは,gdb/remote.c というファイルにある.参考までに,以下が gdb-6.7 に付属する remote.c である.

で,まあこのソースをぜんぶしっかりと読んで解釈すれば言うことは無いのだけど,なにせ7000行近いので,ちょっと面倒だ.まずは ThreadInfo コマンドについて調べようと思い,ThreadInfo で検索をかけると以下のコメントがある.

/* Should we try the 'ThreadInfo' query packet?

This variable (NOT available to the user: auto-detect only!)
determines whether GDB will use the new, simpler "ThreadInfo"
query or the older, more complex syntax for thread queries.
This is an auto-detect variable (set to true at each connect,
and set to false when the target fails to recognize it). */

まあ要約すると,スレッド情報を得るのに ThreadInfo コマンドを利用するとシンプルなのだけど,ThreadInfo コマンドが利用できない場合には,もっと複雑な文法の方法でスレッド情報を取得する,ということだ.なので ThreadInfo コマンドを実装するか,もしくはここで言うところの「もっと複雑な文法の方法」を実装するか,ということになる.

さらに ThreadInfo で検索をかけて調べると,remote_threads_info() という関数の中で ThreadInfo コマンドを発行している.

static void
remote_threads_info (void)
{
struct remote_state *rs = get_remote_state ();
char *bufp;
int tid;

if (remote_desc == 0) /* paranoia */
error (_("Command can only be used when connected to the remote target."));

if (use_threadinfo_query)
{
putpkt ("qfThreadInfo");
getpkt (&rs->buf, &rs->buf_size, 0);
bufp = rs->buf;
if (bufp[0] != '\0') /* q packet recognized */
{
...

で,ここの応答解釈部分を読んでそーいうような応答を返すようにスタブ側を実装すればいいのだけれど,なんか ThreadInfo コマンドは新しい方法のように思える(まず先に ThreadInfo を利用しようとするあたりがそう感じる).で,とりあえずレガシーな方法のほうが移植性とかもいいかなーと思う.(ついでにいうなら remote_threads_info() のコメントによれば,ThreadInfo コマンドは CISCO によって追加された新しいプロトコルのようだ)

remote_threads_info() の内部で ThreadInfo コマンドに失敗した($#00が返ってきた)場合には

/* Else fall back to old method based on jmetzler protocol. */
use_threadinfo_query = 0;
remote_find_new_threads ();
return;
}

のようにして remote_find_new_threads() が呼ばれる.remote_find_new_threads() からは

static void
remote_find_new_threads (void)
{
remote_threadlist_iterator (remote_newthread_step, 0,
CRAZY_MAX_THREADS);
if (PIDGET (inferior_ptid) == MAGIC_NULL_PID) /* ack ack ack */
inferior_ptid = remote_current_thread (inferior_ptid);
}

...

static int
remote_threadlist_iterator (rmt_thread_action stepfunction, void *context,
int looplimit)
{
int done, i, result_count;
int startflag = 1;
int result = 1;
int loopcount = 0;
static threadref nextthread;
static threadref resultthreadlist[MAXTHREADLISTRESULTS];

done = 0;
while (!done)
{
if (loopcount++ > looplimit)
{
result = 0;
warning (_("Remote fetch threadlist -infinite loop-."));
break;
}
if (!remote_get_threadlist (startflag, &nextthread, MAXTHREADLISTRESULTS,
&done, &result_count, resultthreadlist))
{
...

のようにして,さらにremote_threadlist_iterator() → remote_get_threadlist() と呼ばれる.remote_get_threadlist() というのはいかにもそのまんまの名前なので,ここでスレッド一覧を取得しているのだろーなーと推測できる.

さらによく読むと,remote_get_threadlist() からは

static int
remote_get_threadlist (int startflag, threadref *nextthread, int result_limit,
int *done, int *result_count, threadref *threadlist)
{
struct remote_state *rs = get_remote_state ();
static threadref echo_nextthread;
int result = 1;

/* Trancate result limit to be smaller than the packet size. */
if ((((result_limit + 1) * BUF_THREAD_ID_SIZE) + 10) >= get_remote_packet_size ())
result_limit = (get_remote_packet_size () / BUF_THREAD_ID_SIZE) - 2;

pack_threadlist_request (rs->buf, startflag, result_limit, nextthread);
putpkt (rs->buf);
getpkt (&rs->buf, &rs->buf_size, 0);

*result_count =
parse_threadlist_response (rs->buf + 2, result_limit, &echo_nextthread,
threadlist, done);
...

のようにして pack_threadlist_request() という関数が呼ばれていて,ここで

static char *
pack_threadlist_request (char *pkt, int startflag, int threadcount,
threadref *nextthread)
{
*pkt++ = 'q'; /* info query packet */
*pkt++ = 'L'; /* Process LIST or threadLIST request */
pkt = pack_nibble (pkt, startflag); /* initflag 1 bytes */
pkt = pack_hex_byte (pkt, threadcount); /* threadcount 2 bytes */
pkt = pack_threadid (pkt, nextthread); /* 64 bit thread identifier */
*pkt = '\0';
return pkt;
}

のようにして qL コマンドが発行されている.さらにその応答を parse_threadlist_response() という関数で解析している.

static int
parse_threadlist_response (char *pkt, int result_limit,
threadref *original_echo, threadref *resultlist,
int *doneflag)
{
struct remote_state *rs = get_remote_state ();
char *limit;
int count, resultcount, done;

resultcount = 0;
/* Assume the 'q' and 'M chars have been stripped. */
limit = pkt + (rs->buf_size - BUF_THREAD_ID_SIZE);
/* done parse past here */
pkt = unpack_byte (pkt, &count); /* count field */
pkt = unpack_nibble (pkt, &done);
/* The first threadid is the argument threadid. */
pkt = unpack_threadid (pkt, original_echo); /* should match query packet */
while ((count-- > 0) && (pkt < limit))
{
pkt = unpack_threadid (pkt, resultlist++);
if (resultcount++ >= result_limit)
break;
}
...

parse_threadlist_response()の内部では,while によって応答内容を解析し,スレッドIDを取得している.ということは,複数スレッドの情報をまとめて返すことができるようだ.(さらにremote_threadlist_iterator() から stepfunction 引数経由でremote_newthread_step() が実行され,add_thread()によりスレッドが追加されるようだ)

なお remote_threadlist_iterator() からは while ループによってremote_get_threadlist() を繰り返し呼んでいるが,最初は startflag を1,その後は0として繰り返し qL コマンドを発行し,それに対する応答中の done フラグが立ったときに qL の発行を終了する,ということがわかる.

つまり,以下のようなことが推測できる.
  • qLコマンドによりスレッド情報を取得する.複数スレッドの情報をまとめて取得できる.
  • しかしスレッド数は場合によっては100近くになったりすることもあり,1回のqLコマンド発行で全スレッドの情報が送りきれないことが考えられる.(たとえばスタブ上では応答用のバッファ領域として remcomOutBuffer[] を定義しているが,このサイズは i386-stub.c では #define BUFMAX 400 となっており,けっこう少ない)
  • 一度に送りきれない場合には,複数回に分割して送る.gdb側からqLコマンドを最初に発行する際には,start フラグを1としてqLコマンドを発行する.
  • スタブ側では start フラグが1のqLコマンドを受信したら,スレッド情報を最初から送る.start フラグが0のqLコマンドを受信したら,前回送信したスレッドの次のスレッド情報を送る.
  • スタブ側では,スレッド情報を送りきったら done フラグを1にして応答を返す.gdbは done フラグが1になっている応答が返されるまで,qLコマンドを繰り返し送信する.
ちなみに同時に送れるスレッドの最大数は,remote.c で MAXTHREADLISTRESULTSとして定義されている.しかしこの値は32と,けっこう少ない.この32という値は qL コマンドの発行時にパラメータとして送信されるので,スタブ側ではこの値よりも多いスレッドを詰め込んで返してはいけないようだ.

ということで,qL コマンドに対する応答処理を実装してみよう.まず実際の通信だが,上で試した結果では

$qL1200000000000000000#50

というものがgdbから送信されている.remote.c の内部でこれを送信しているのは上で説明したようにpack_threadlist_request() だが,ここでの処理を見ると

static char *
pack_threadlist_request (char *pkt, int startflag, int threadcount,
threadref *nextthread)
{
*pkt++ = 'q'; /* info query packet */
*pkt++ = 'L'; /* Process LIST or threadLIST request */
pkt = pack_nibble (pkt, startflag); /* initflag 1 bytes */
pkt = pack_hex_byte (pkt, threadcount); /* threadcount 2 bytes */
pkt = pack_threadid (pkt, nextthread); /* 64 bit thread identifier */
*pkt = '\0';
return pkt;
}

となっており,コマンドのフォーマットは以下のようになっているようだ.
  • コマンドの先頭は qL で始まる.
  • qLの直後に16進数1桁で start フラグを付加する.
  • その直後に16進数2桁でスレッドの最大数を付加する.(スタブからの応答時には,この数を超えてスレッド情報を詰め込んではいけない.ここには MAXTHREADLISTRESULTS の値が埋め込まれる)
  • その直後に16進数で16桁で,前回のqLコマンドで最後に取得したスレッドIDが付加される(この値は copy_threadref() により,nextthread に保存されている).スタブ側では,ここで与えられたスレッドIDの次のスレッドから応答する.
さらに parse_threadlist_response() を見ると,その応答は以下のようなフォーマットになっているらしい.
  • コマンドの先頭は qM で始まる.
  • その直後に16進数2桁で,スレッド数が格納される.
  • その直後に16進数1桁で,done フラグが格納される.
  • その直後に16進数16桁で,qLコマンドで送信されたスレッドIDがそのまま格納される.この値は remote_get_threadlist() 内で

    if (!threadmatch (&echo_nextthread, nextthread))

    のようにして一致するかチェックされているので,そのまま返さないとエラーになる.
  • その直後から,スレッド数ぶんのスレッドIDが連続して,16進数16桁で格納される.
さて,KOZOSのスレッド数は現状ではせいぜい10を超えることは無い.なのでいっぺんにぜんぶ詰め込んで返すのが,一番簡単だ.しかしこれだと,スレッド数が増えた際にまた別途対応が必要になってしまう.

次に簡単なのは,qLコマンドが来るたびにひとつずつスレッドIDを返すことだ.これならばスレッド数が増えても問題は無い.まあ本来ならば32個までスレッドを格納して,それを超えたら分割して返すようにするのが効率が良いのだが,めんどうなのでひとつずつ返す実装にする.

ということで実装してみた.以下は今回実装した,スタブに対する差分.

case 'q':
switch (*ptr++)
{
case 'L':
{
int startflag, doneflag, countmax, count = 1, i;
unsigned int threadid[2];
kz_thread *thp;

startflag = hex(*ptr++);
countmax = hex(*ptr++) << 4;
countmax += hex(*ptr++);

hex2mem(ptr, threadid, 8, 0);

doneflag = 1;
for (i = 0; i < THREAD_NUM; i++) {
thp = &threads[i];
if (!thp->id) continue;
if ((unsigned int)thp->id > threadid[1]) {
/* 前回の次のスレッド */
doneflag = 0;
break;
}
}

ptr = remcomOutBuffer;
*ptr++ = 'q';
*ptr++ = 'M';
*ptr++ = hexchars[count >> 4];
*ptr++ = hexchars[count & 0xf];
*ptr++ = doneflag ? '1' : '0';
ptr = mem2hex(threadid, ptr, 8, 0);

if (!doneflag) {
threadid[0] = 0;
threadid[1] = (int)thp->id;
ptr = mem2hex(threadid, ptr, 8, 0);
}
*ptr++ = '\0';
}
break;
default:
break;
}

前回のスレッドIDを覚えておいて,start フラグが立っているならば最初のスレッド,そうでないならば前回の次のスレッドを返す,という実装にしてもいいのだけど,qLコマンドで前回送信したスレッドの最後のIDが返ってくる(remote_threadlist_iterator() 内での copy_threadref() の呼び出し参照)ので,その次のスレッドを検索して返すだけのシンプルな実装にしてみた.まあ上のコードには実はちょっとバグがあるのだけどそのへんは次回に譲るとして,さっそく動作させてみよう.

さっきと同じように実行形式 koz を起動,gdbで接続して continue,Ctrl-C でブレーク.さらに info threads を実行してみる.

画像はこちら

おー,なんかそれっぽい値が出ている.現時点でのスレッド数はextintr, stubd, outlog, idle, clock, telnetd, httpd の7つだが,info threads によって7つのスレッド情報っぽいものが出ているので,とりあえずはそれっぽく動いているようだ.しかしすべてのスレッドについて

breakpoint () at i386-stub.c:1021

となっている.これはおそらくスレッドの現在の停止位置を指しているのだと思われるが,ぜんぶ同じ箇所で止まっている(通常のスレッドは syscall.c のkz_syscall()あたりで止まっているように思われるから,これはきっとおかしい).

このときのgdbとスタブの通信は以下.

[$qfThreadInfo#bb](+)($#00)[+]
[$qL1200000000000000000#50](+)($qM01000000000000000000000000060330c08#96)[+]
[$qL0200000000060330c08#96](+)($qM0100000000060330c080000000060360c08#e0)[+]
[$qL0200000000060360c08#99](+)($qM0100000000060360c080000000060390c08#e6)[+]
[$qL0200000000060390c08#9c](+)($qM0100000000060390c0800000000603c0c08#13)[+]
[$qL02000000000603c0c08#c6](+)($qM01000000000603c0c0800000000603f0c08#40)[+]
[$qL02000000000603f0c08#c9](+)($qM01000000000603f0c080000000060420c08#10)[+]
[$qL0200000000060420c08#96](+)($qM0100000000060420c080000000060450c08#e0)[+]
[$qL0200000000060450c08#99](+)($qM0110000000060450c08#9a)[+]
[$qC#b4](+)($#00)[+]
[$qThreadExtraInfo,60450c08#4f](+)($#00)[+]
[$qP0000001f0000000060450c08#c2](+)($#00)[+]
[$Hg60450c08#79](+)($#00)[+]
[$g#67](+)($00780d08000000004304000060360c0858e60e0858e60e08ece70e080000000059aa040802020000330000003b0000003b0000003b0000003b0000001b000000#18)[+]
[$qP0000001f0000000060420c08#bf](+)($#00)[+]
[$Hg60420c08#76](+)($#00)[+]
[$g#67](+)($00780d08000000004304000060360c0858e60e0858e60e08ece70e080000000059aa040802020000330000003b0000003b0000003b0000003b0000001b000000#18)[+]
[$qP0000001f00000000603f0c08#f2](+)($#00)[+]
[$Hg603f0c08#a9](+)($#00)[+]
[$g#67](+)($00780d08000000004304000060360c0858e60e0858e60e08ece70e080000000059aa040802020000330000003b0000003b0000003b0000003b0000001b000000#18)[+]
[$qP0000001f00000000603c0c08#ef](+)($#00)[+]
[$Hg603c0c08#a6](+)($#00)[+]
[$g#67](+)($00780d08000000004304000060360c0858e60e0858e60e08ece70e080000000059aa040802020000330000003b0000003b0000003b0000003b0000001b000000#18)[+]
[$qP0000001f0000000060390c08#c5](+)($#00)[+]
[$Hg60390c08#7c](+)($#00)[+]
[$g#67](+)($00780d08000000004304000060360c0858e60e0858e60e08ece70e080000000059aa040802020000330000003b0000003b0000003b0000003b0000001b000000#18)[+]
[$qP0000001f0000000060360c08#c2](+)($#00)[+]
[$Hg60360c08#79](+)($#00)[+]
[$g#67](+)($00780d08000000004304000060360c0858e60e0858e60e08ece70e080000000059aa040802020000330000003b0000003b0000003b0000003b0000001b000000#18)[+]
[$qP0000001f0000000060330c08#bf](+)($#00)[+]
[$Hg60330c08#76](+)($#00)[+]
[$g#67](+)($00780d08000000004304000060360c0858e60e0858e60e08ece70e080000000059aa040802020000330000003b0000003b0000003b0000003b0000001b000000#18)[+]
[$Hg0#df](+)($#00)[+]
[$g#67](+)($00780d08000000004304000060360c0858e60e0858e60e08ece70e080000000059aa040802020000330000003b0000003b0000003b0000003b0000001b000000#18)[+]
(この状態で停止)

まず最初のほうで qL コマンドを繰り返し発行することで,スレッド情報が取得できている.最初のqLコマンドのみ start フラグが立っており,最後の qM による返信のみ done フラグが立っていることに注目.

さらにその後,qCとかHgとかのコマンドが発行されている.

注意すべきは Hg コマンドの後に g コマンドでレジスタ情報を取得していることだ.Hg コマンドの引数は,値を見た感じではどうもスレッドIDのようだ(リトルエンディアンになっているので注意).で,これらのコマンドは繰り返し発行されているので,おそらく各スレッドのレジスタ情報(コンテキスト情報)を取得しようとしているのだとおもわれる.ところが現状のKOZOSのスタブでは,gコマンドによるレジスタ値取得では現在停止中のスレッドのレジスタ値を返すように実装されているので,毎回同じ値が返ることになる.このため,すべてのスレッドにおいて同じ箇所で止まっているように見えてしまっているのだろう.おそらく Hg で渡されたスレッドIDに対してカレントスレッドを切替えて,で,g コマンド受信時にはカレントスレッドのレジスタ値を返すような実装にする必要があるのだろう.

ついでにいうと,gdb側では

1 Thread 1613958152 breakpoint () at i386-stub.c:1021

のように表示されていて,スレッドIDがたとえば 1613958152 のような値になっているが,これは16進数にすると 0x60330c08 となる.エンディアンをひっくりかえすと0x080c3360 となりそれっぽい値になるので,エンディアンを合わせてやらなければならないようだ.次回はこのへんを実装してみよう.
(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

ここまでで,デバッガとしての機能はだいたい動くことが確認できた.今回はちょっと毛色を変えて,デバッガのGUIについて.

まあ最近はデバッガもコンソール上での原始的なものではなく,GUIを使ったきらびやかなものもあるというよりもむしろそっちのほうが今となっては主流のような気もする.まあそうはいっても emacs 上でも十分であったり,むしろそちらのほうが使いやすい場合も多かったりするのでどちらも一長一短あり,というか好きずきなような気もするが,GUIを利用したデバッガについてちょっと説明しよう.

gdbはコマンド操作によるデバッガなので,実はGUI上でボタンを操作したらgdbのコマンドを発行するだけ(そしてその応答を見て,GUI上にきれいに表示する)ということをすれば,GUIは作成できる.つまりGUIは基本的にはgdbとは独立して,ボタン押下などのイベントをgdbコマンドに変換してコマンド発行するだけのツール,ということもできる.こーいうのをよく「gdbの上にかぶせてある」とか「GUIの皮をかぶせてある」とか「単なるラッパ(wrapper)」なんていうふうに表現する.

で,gdbのGUIとしては,dddというものがある.これはData Display Debugger の略で,その名の通り,データ構造を表示するのに威力を発揮する.まあ実際のところ,単にコマンド発行するだけならばCUIでもGUIでもたいして変わりはなく,使う本人が操作しやすいかどうか,あとはGUIならばコマンド名を覚えなくても済む,くらいの違いしか無いような気もするが,データ構造の表示に関していえば,GUIによるグラフィカル表示は非常にわかりやすく,圧倒的に便利だと思う.(まあCUIでもそーいう表示はやってできなくはないとは思うが)

ほかにも GUI としては kdbg とか kdevelop とか insight とかいろいろあるようなのだが,今回はdddをちょっと使ってみよう.

まず ddd の準備なのだが,FreeBSD の ports になっているので,ふつうにパッケージインストールできる.ports のカテゴリは devel だ.ちなみに ddd は gdb に対する純粋なラッパなので,たとえばクロス開発でターゲット機器用のクロスgdbを利用している場合にも,通常の ddd を使う(そして起動時に --debugger オプションで使いたい gdb を指定する)ことができる.つまり,ddd自身はクロスコンパイルの必要は無い.(そもそもdddはgdbに対する「皮」だけなので,クロスコンパイルという概念が無い)

で,ddd をインストールしたら,実行形式 koz を起動する.KOZOSのソースコードは前回と同じものを使用する.

% ./koz
(この状態で停止)

koz の起動時には,デバッガからの接続待ちで止まっている.いつもならばここで gdb から target コマンドでリモート接続することになるが,今回は ddd を起動しよう.引数に実行形式 koz を指定して起動する.

% ddd koz
Creating "/home/hiroaki/.ddd/"...
Creating "/home/hiroaki/.ddd/"...done.
Creating "/home/hiroaki/.ddd/sessions/"...
Creating "/home/hiroaki/.ddd/sessions/"...done.
Creating "/home/hiroaki/.ddd/themes/"...
Creating "/home/hiroaki/.ddd/themes/"...done.



画像はこちら

tipsとかヘルプがいろいろ開いているのでとりあえずクローズして,メインウインドウにする.

画像はこちら

で,まずはターゲットへの接続なのだが,この接続の設定のしかたがよくわからん.まあいざとなったら .gdbinit に target remote を書いておく,というのでもいいのだが,今回はコマンド実行用のウインドウから target remote を実行してみる.

まず,メニューバーから Commands - Clear Window を実行してコマンド実行ウインドウをクリアする.なぜかこれをやらないとコマンド実行できない.

画像はこちら

一番下のコマンド実行用ウインドウがクリアされていることに注意.

で,コマンド実行用ウインドウで target remote コマンドを実行し,スタブに接続する.

画像はこちら

おー,ブレーク位置のソースコードが出てきた.どうやら接続できたようだ.

ちなみに簡単な操作用に

画像はこちら

のようなツールバーが出ているが,ここで Cont を押下して continue を行う.

画像はこちら

コマンド実行用ウインドウで continue が行われていることに注意.

Wed Nov 14 21:45:21 2007
Wed Nov 14 21:45:22 2007
Wed Nov 14 21:45:23 2007
Wed Nov 14 21:45:24 2007
Wed Nov 14 21:45:25 2007
Wed Nov 14 21:45:26 2007
...

時刻表示が開始される.KOZOSが動作を開始したわけだ.

さてここで,いままでどおりブレークポイントの設定やステップ実行もできるのだが,それではあまり面白くない.まあこのへんのことはやればわかるだろうし,せっかくGUIを使っているので,データ構造の表示をしてみたいところだ.

まずツールバーで Interrupt を押下して,実行を停止する.どうやらこれは Ctrl-C に相当するようだ.

画像はこちら

Wed Nov 14 21:48:36 2007
Wed Nov 14 21:48:37 2007
Wed Nov 14 21:48:38 2007
($T054:58e60e08;5:58e60e08;8:c5a80408;#c0)[+]
[$M8048088,1:55#c2](+)($OK#9a)[+]
(この状態で停止)

時刻表示が停止し,gdbとスタブ間の通信が始まっている.

例として,KOZOSが持っている優先度キュー情報を見てみよう.優先度キューの配列名は readyque なので,左上の検索バーに「readyque」と入力してみる.

画像はこちら

おー,readyque の定義場所が表示された.

ここでソースコード上の「readyque」をおもむろにダブルクリックすると...

画像はこちら

なんか上のほうにウインドウができているね.ちょっと広げてみよう.

画像はこちら

おー,配列が表示されている.

ここで readyque の配列に値が入っている部分は,優先度キューでスレッドが存在するプライオリティだ.で,そこをダブルクリックする.

画像はこちら

なんと,リンクしているスレッド構造体が表示される.ほかのところもクリックしてみよう.

画像はこちら

こんな感じで,構造体のリンク構造とかを知ることができる.上の例だと優先度2のレディーキューに stubd,優先度31のレディーキューに idle スレッドが存在しているようだ.telnetd とか httpd も存在はしているが,メッセージ受信待ち状態のためレディーキューには接続されていないので,ここでは表示されない.

たとえば他にもタイマ管理なら,timers をダブルクリックして

画像はこちら

みたいにして,タイマのリンク構造とさらにそこからリンクされているスレッドの構造体を見ることができる.こんなふうに構造体のリンク構造を追いかけていくことができるわけだ.まあこの例だとタイマがひとつしか登録されていないのだが,タイマが複数登録されている場合には,タイマ構造体の next をクリックしていけば,リンクリストを追いかけて表示することができる.

このへんの値は,実はまあgdb上でも

(gdb) print readyque[2]
(gdb) print *(readyque[2])
(gdb) print *(readyque[2]->next)
(gdb) print *timers
(gdb) print *(timers->next)
(gdb) print *(timers->next->next)

とかやれば表示させることはできる.gdb上では,データの表示にC言語の演算子がほぼそのまま使えるので,ポインタのリンク先などをC言語と同様の書き方で追うことができる.*p とか p->next とか &p とか sizeof(*p) とかいった書き方もそのままできる.で,表示対象が構造体の場合にはそのメンバ値を全部表示してくれるし,文字列の場合にはその内容をきちんと表示してくれる.

しかし ddd のように,リンク構造を図示してくれるというのは非常にありがたい.ハッシュや二分木,2重リンク,リンクのリンク,もしくは共用体が入り組んでいたりして,複雑になっているデータ構造を追う場合にはとてもべんりだ.プログラムは機能追加につれてどんどん新しいポインタが追加され,データ構造は複雑になっていきがちなものだからだ.個人的な感想としては,このためだけに ddd を使ったとしても十分な価値があると思う.

う~ん,現代的だ.やっぱし今の時代,こうでなくっちゃあねえ.