(第19回)GDBのスレッド対応(その2:エンディアンの問題)

2007/11/19

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

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

まずエンディアンについてだけど, 前回の結果では,スレッド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の送受信部分はビッグエンディアン使用に 書き換える必要がある.で,修正したのが以下.

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

で,以下が今回の修正ぶんだ.ちなみに今回は,修正はスタブのみ.

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 とかいったコマンドがスタブで処理できていないのだが, このへんはまた次回!
メールは kozos(アットマーク)kozos.jp まで