GDBでGo言語のスライスの値をのぞいてみた

最近Go言語をよく触っています。触っているうちに、スライス、文字列、インターフェースなどの値がメモリ上でどう表現されているか知っておくと実装やデバッグ時に役立ちそうだなーと思うことが度々でてきました。そこでGo言語本体のソースコードを読みつつ、そのあたりをGDBでみてみました。そのときのメモです。今回はまず、スライスと文字列についてです。

使用するデバッガ

そもそもGo言語のデバッガについてよく知らなかったので、どんなのがあるか調べてみました。有名なのは次の3つのようです。

  • godebug
  • delve
  • GDB

機能的にはdelveが一番リッチな感じでしたが、今回は簡単にメモリを覗けそうなGDBにしました。

ちなみにOSはMac OS X 64bit、Goのバージョンは1.6.2です。

スライスの値

スライスは次のような構造体をもっています(ソース)。

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

arrayは配列へのポインタ、lenはスライスが保持している値のリストの長さ、capは(arrayが指すアドレスを始点とした)配列の長さです。lencapの違いについてはGo Blogを読むのがいいかと思います。

スライスの値をGDBでみるにあたり、シンプルな例として次のようなコードを使います。

package main

func main() {
  s := []int{1, 2, 3}
  _ = s
}

GoのバイナリをGDBから扱いやすくするため、次のようにgcflagsオプションをつけてビルドします。

go build -gcflags "-N -l" slice1.go

GDBを起動して、スライスの値をみてみます。// 以降の文字は後で書き足しています。

% gdb ./slice1
...省略...
(gdb) break main.main  // main関数にbreakpointを設置
Breakpoint 1 at 0x2040: file /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice1.go, line 3.
(gdb) run  // 実行
Starting program: /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice1
[New Thread 0x1313 of process 64589]
[New Thread 0x1403 of process 64589]
[New Thread 0x1503 of process 64589]

Breakpoint 1, main.main () at /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice1.go:3
3       func main() {  // breakpointで止まった行
(gdb) next  // 次の行へ
4         s := []int{1, 2, 3}
(gdb) next  // 次の行へ
6       }  // 今はこの行の実行前
(gdb) print &s  // 変数sのアドレスを確認
$1 = (struct []int *) 0xc820035f30
(gdb) x/3gx 0xc820035f30  // 変数sの値を確認(1)
0xc820035f30:   0x000000c820035f10      0x0000000000000003
0xc820035f40:   0x0000000000000003
(gdb) x/3gx 0xc820035f10  // arrayの中身を確認
0xc820035f10:   0x0000000000000001      0x0000000000000002
0xc820035f20:   0x0000000000000003

(1)の結果から、変数sに配列へのポインタ、長さ、キャパシティが格納されているのがわかります。

スライスを関数の引数にする

次に、関数の引数にスライスを与えたとき、スライスがどうなるか確認してみます。次のコードを使います。

package main

func add4(s []int) []int {
  s = append(s, 4)
  return s
}

func main() {
  s := []int{1, 2, 3}
  s = add4(s)
}

ビルドして、GDBを起動します。

% gdb ./slice2
...省略...
(gdb) break main.main  // main関数にbreakpointを設置
Breakpoint 1 at 0x2170: file /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice2.go, line 8.
(gdb) run  // 実行
Starting program: /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice2
[New Thread 0x1313 of process 68743]
[New Thread 0x1403 of process 68743]
[New Thread 0x1503 of process 68743]

Breakpoint 1, main.main () at /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice2.go:8
8       func main() {  // breakpointで止まった行
(gdb) next  // 次の行へ
9        s := []int{1, 2, 3}
(gdb) next  // 次の行へ
10        s = add4(s)  // 今はこの行の実行前
(gdb) print &s  // main関数内の変数sのアドレスを確認
$1 = (struct []int *) 0xc820035f30
(gdb) step  // add4関数に入る
main.add4 (s=..., ~r1=...) at /Users/yagami/src/go-lib/src/github.com/ks888/hello/slice2.go:3
3       func add4(s []int) []int {
(gdb) next  // 次の行へ
4         s = append(s, 4)
(gdb) next  // 次の行へ
5         return s  // 今はこの行の実行前
(gdb) print &s  // add4関数内の変数sのアドレスを確認
$3 = (struct []int *) 0xc820035ee0
(gdb) x/3gx 0xc820035ee0  // add4関数内の変数sの値を確認(3)
0xc820035ee0:   0x000000c82000e060      0x0000000000000004
0xc820035ef0:   0x0000000000000006
(gdb) x/3gx 0xc820035f30  // main関数内の変数sの値を確認(4)
0xc820035f30:   0x000000c820035f10      0x0000000000000003
0xc820035f40:   0x0000000000000003

ポイントとしては、add4関数内の変数sの値(3)と、main関数内の変数sの値(4)が異なっているところです。add4関数内の変数sは、appendによりlenが4になっています。また、append時に配列の再確保が行われ、caparrayの値も変わっています。一方で、main関数内の変数sはそのままです。ここから、関数の引数にスライスを与えると、スライスの値はコピーされることがわかります。

文字列の値

ついでに文字列の値もみてみます。文字列は次のような構造体をもっています(ソース)。

type stringStruct struct {
    str unsafe.Pointer
    len int
}

strは配列へのポインタ、lenは配列の長さです。

今度は次のコードを使います。

package main

func main() {
  str := "Hello World"
  _ = str
}

ビルドして、GDBで見てみます。

% gdb ./string
(gdb) break main.main  // main関数にbreakpointを設置
Breakpoint 1 at 0x2040: file /Users/yagami/src/go-lib/src/github.com/ks888/hello/string.go, line 3.
(gdb) run  // 実行
Starting program: /Users/yagami/src/go-lib/src/github.com/ks888/hello/string
[New Thread 0x1313 of process 74658]
[New Thread 0x1403 of process 74658]
[New Thread 0x1503 of process 74658]

Breakpoint 1, main.main () at /Users/yagami/src/go-lib/src/github.com/ks888/hello/string.go:3
3       func main() {  // breakpointで止まった行
(gdb) next  // 次の行へ
4         str := "Hello World"
(gdb) next  // 次の行へ
6       }
(gdb) print &str  // 変数strのアドレスを確認
$2 = (struct string *) 0xc82003bf38
(gdb) x/2gx 0xc82003bf38  // 変数strの値を確認
0xc82003bf38:   0x0000000000071ca0      0x000000000000000b
(gdb) x/2gx 0x0000000000071ca0  // 変数str内のポインタの値を確認
0x71ca0 <go.string.*+8024>:     0x6f57206f6c6c6548      0x0000000000646c72

変数strの値をみると、配列へのポインタ(0x0000000000071ca0)と、配列の長さ(0x000000000000000b)が格納されています。また、ポインタの指す先をみてみると、リトルエンディアンな上16進数で見づらいですが、Hello Worldの文字列が確認できます。

感想

こういう風に理解しておくと、スライスで迷いにくくなるのでは、と期待しています。次はインターフェースがメモリ上でどう表現されているかについて書こうと思います。

参考にした記事