読者です 読者をやめる 読者になる 読者になる

Goのmainが呼ばれるまでの道のり0日目と1日目

Go

0日目
Twitterで64itのLinuxでgccgoをビルドする方法を教えてもらいました。
ありがとう@apkerさん!!
まずは、ビルドから。

基本的にはSetting up and using gccgoにしたがえば良いんだけど、
その場合、multilibのビルドに失敗して上手く行かない場合があるみたいです。(というか、自分も教えてくれた人もそこで引っかかってた)
なので、configureのオプションに、「--disable-multilib」をつけて失敗する箇所を回避できました。

自分は既存の環境にgccgoを入れるつもりはなかったので、debootstrapをつかってgccgo用に環境つくりました。
chroot環境なので、めんどくさいし一般ユーザ作らずにrootで全部やってます。
#あ、家で使ってるのはDebian sid 64bitですが、chroot環境はlenny 64bitにしました。。

sudo debootstrap lenny . http://ftp.jp.debian.org/debian 

chroot環境ができたら、ビルド環境を作って行きます。
ディレクトリ構成はこんな感じです。

/root -- gccgo
      |- gccgo-src
      |- objdir

・gccgoディレクトリはmake installしたときにバイナリだとかをインストールする場所
・gccgo-srcはsvnから取ってきたソースの場所
・objdirはビルド用のディレクトリ

そして、gccのビルドに必要なパッケージ達をインストール。
gccとmakeの他に必要なパッケージはこれらなのでdeban/ubuntuの人はaptitudeでインストールできます。

libgmp3-dev libgmpxx4ldbl libmpfr-dev flex

これで、ビルドする準備ができたので、objdirに移動します。
そして、configureを実行。

../gccgo-src/configure --disable-multilib --enable-languages=c,c++,go --prefix=/root/gccgo

ビルドに必要なパッケージが揃ってればエラーは出ないはずなので、makeします。
多分これもエラーは出ずに終了すると思うので、make installでgccgoをインストールしますよ。

さて、これでインストールできたんですが、お分かりの用に通常のディレクトリ(/usrとか)にインストールしてないので、
PATHを通したりが必要なので、適当にセットします。

PATH=$PATH:/root/gccgo/bin/ ; export PATH

そして、ついにhello worldコンパイルする時がきました。

# cat test.go
package main

import "fmt"

func main()
{
	fmt.Printf("hello world\n")
}

これをこんな感じでビルドして、

#gccgo test.go 

こんな感じで実行できます。

# LD_LIBRARY_PATH=./gccgo/lib64 ./a.out
hello world
#

キタ――(゜∀゜)――!!

これで、やっと準備が整ったので本題に入っていくと、Goの実行時にtest.goがどんな風に動くか見たかったので、gdb使って見てきます。

1日目
Goの処理を追うのに実際のバイナリを動かしつつ、gdbでソースを読んでます。
test.goを-g付けてデバッグ情報付きでコンパイルして、gdb実行します。
LD_LIBRARY_PATHが邪魔なら、適当に設定すると楽です。

# LD_LIBRARY_PATH=./gccgo/lib64 gdb ./a.out

起動したらまずは、listコマンドです。
何回かエンターキーを押下するとこんな感じになります。

(gdb) list
19	
20	extern struct __go_open_array Args asm ("os.Args");
21	
22	extern struct __go_open_array Envs asm ("os.Envs");
23	
24	extern void real_main () asm ("main.main");
25	
26	int
27	main (int argc, char **argv)
28	{
(gdb) 
29	  int i;
30	  struct __go_string **values;
31	
32	  Args.__count = argc;
33	  Args.__capacity = argc;
34	  values = __go_alloc (argc * sizeof (struct __go_string *));
35	  for (i = 0; i < argc; ++i)
36	    {
37	      size_t len;
38	      struct __go_string *s;
(gdb) 
39	
40	      len = __builtin_strlen (argv[i]);
41	      s = __go_alloc (sizeof (struct __go_string) + len);
42	      s->__length = len;
43	      __builtin_memcpy (&s->__data[0], argv[i], len);
44	      values[i] = s;
45	    }
46	  Args.__values = values;
47	
48	  for (i = 0; environ[i] != NULL; ++i)
(gdb) 
49	    ;
50	  Envs.__count = i;
51	  Envs.__capacity = i;
52	  values = __go_alloc (i * sizeof (struct __go_string *));
53	  for (i = 0; environ[i] != NULL; ++i)
54	    {
55	      size_t len;
56	      struct __go_string *s;
57	
58	      len = __builtin_strlen (environ[i]);
(gdb) 
59	      s = __go_alloc (sizeof (struct __go_string) + len);
60	      s->__length = len;
61	      __builtin_memcpy (&s->__data[0], environ[i], len);
62	      values[i] = s;
63	    }
64	  Envs.__values = values;
65	
66	  srandom ((unsigned int) time (NULL));
67	
68	  real_main ();
(gdb) 
69	
70	  return 0;
71	}
(gdb) 

test.goで定義したmain()はどれやねん?となるんですが、それはreal_main()です。
int main(int argc, char **argv)はGo本体の処理です。

func main()と書いたほうのmain()は下のように定義されてます。

extern void real_main () asm ("main.main");

最後の方でreal_main()の呼び出しがあります。

このmain()がやってるのは、引数(argc、argv、環境変数)をreal_main側で使えるようにするための準備です。
GoのチュートリアルにEchoの例があって、
それでユーザが入力した引数を出力してくサンプルがあるんですが、
そのサンプルコードでimportしてるライブラリにosというのがいます。
その中で、ユーザから入力された引数を扱っているのが多分以下の構造体(Argsのほう)です。

extern struct __go_open_array Args asm ("os.Args");
extern struct __go_open_array Envs asm ("os.Envs");

ここのコードは別に大したことはしていなくて、入力された引数をArgsにコピーしたり、
環境変数をEnvsにコピーしたりするだけです。

引数・環境変数の設定が終わったらreal_main(func main()として作った関数)を呼んであげます。

.goにいるmain()の呼び出し手順は以上なんですが、__go_alloc()は単なるmallocのラッパーじゃなくて、
要求されたサイズに応じてどこからメモリを確保するかなどを決定する処理をしてます。
なのでこちらを見ていくと、この関数の全体はこんな感じです。

(gdb) list 21
16	
17	// Allocate an object of at least size bytes.
18	// Small objects are allocated from the per-thread cache's free lists.
19	// Large objects (> 32 kB) are allocated straight from the heap.
20	void*
21	__go_alloc(uintptr size)
22	{
23		int32 sizeclass;
24		MCache *c;
25		uintptr npages;
(gdb) 
26		MSpan *s;
27		void *v;
28		uint32 *ref;
29	
30		if(m->mallocing)
31			throw("malloc - deadlock");
32		m->mallocing = 1;
33	
34		if(size == 0)
35			size = 1;
(gdb) 
36	
37		if(size <= MaxSmallSize) {
38			// Allocate from mcache free lists.
39			sizeclass = SizeToClass(size);
40			size = class_to_size[sizeclass];
41			c = m->mcache;
42			v = MCache_Alloc(c, sizeclass, size);
43			if(v == nil)
44				throw("out of memory");
45			mstats.alloc += size;
(gdb) 
46		} else {
47			// TODO(rsc): Report tracebacks for very large allocations.
48	
49			// Allocate directly from heap.
50			npages = size >> PageShift;
51			if((size & PageMask) != 0)
52				npages++;
53			s = MHeap_Alloc(&mheap, npages, 0);
54			if(s == nil)
55				throw("out of memory");
(gdb) 
56			mstats.alloc += npages<<PageShift;
57			v = (void*)(s->start << PageShift);
58		}
59	
60		// setup for mark sweep
61		mlookup(v, nil, nil, &ref);
62		*ref = RefNone;
63	
64		m->mallocing = 0;
65		return v;
(gdb) 
66	}

ここで重要な箇所第1段めは37行目の分岐です。
ここで、要求されたサイズがMaxSmallSizeより大きいかどうかでどこからメモリ確保するかが変わります。
MaxSmallSizeの値は32768のようです。

(gdb) printf "%d", MaxSmallSize
32768(gdb) 

これを動かしてたときはsizeの値が24だったので、「// Allocate from mcache free lists.」というほうに入っていきます。

最初にSizeToClass()でこんな感じです。

SizeToClass (size=24) at ../../../gccgo-src/libgo/runtime/msize.c:49
49	{
(gdb) list
44	static int32 size_to_class8[1024/8 + 1];
45	static int32 size_to_class128[(MaxSmallSize-1024)/128 + 1];
46	
47	int32
48	SizeToClass(int32 size)
49	{
50		if(size > MaxSmallSize)
51			throw("SizeToClass - invalid size");
52		if(size > 1024-8)
53			return size_to_class128[(size-1024+127) >> 7];
(gdb) list
54		return size_to_class8[(size+7)>>3];
55	}

SizeToClass()が返す値は、size_to_class128かsize_to_class8のどこかです。どこかというのは、配列のインデックス計算してる部分ですね。
ここも、sizeの大きさによって使う配列が変わり、今回はsize==24なのでsize_to_class8を使います。
配列の中身はこんな感じになってました。

(gdb) p size_to_class8
$4 = {1, 1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 
  20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 24 <repeats 12 times>, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 
  26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29 <repeats 16 times>, 30, 30, 30, 30, 30, 30, 30, 0}
(gdb)

(size+7)>>3の結果は3なので、size_to_class8[3]の値(3)を返します。
そしたら、元の関数に戻って、sizeclass==3となります。

sizeclass = SizeToClass(size);

その後、以下のようにsizeの値を書き換えます。

size = class_to_size[sizeclass];

書き換え後の結果は32になりました。

ここまでが、今日調べたところです〜