(第18回)GDBのスレッド対応(その1:コマンド処理の実装)

2007/11/18

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

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

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

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

GDBには,以下のスレッド管理機能がある. このへんについてはGDBのマニュアル本とかにも説明があるのでそちらを参照.

これらの実現には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 の発行を終了する, ということがわかる.

つまり,以下のようなことが推測できる.

ちなみに同時に送れるスレッドの最大数は,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;
}
となっており,コマンドのフォーマットは以下のようになっているようだ. さらに parse_threadlist_response() を見ると,その応答は以下のような フォーマットになっているらしい. さて,KOZOSのスレッド数は現状ではせいぜい10を超えることは無い. なのでいっぺんにぜんぶ詰め込んで返すのが,一番簡単だ. しかしこれだと,スレッド数が増えた際にまた別途対応が必要になってしまう.

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

ということで実装してみた.

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

以下は今回実装した,スタブに対する差分.

	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 となりそれっぽい値になるので,エンディアンを合わせてやらなければ ならないようだ.次回はこのへんを実装してみよう.
メールは kozos(アットマーク)kozos.jp まで