概要
スライス・string・interface{}の内部構造をプログラミングによって調査していきます。その準備として変数の型の再定義の方法を考えます。最後にGoのソースコードを調べたものも載せました。
環境
$ go version
go version go1.12 windows/amd64
準備
変数が占めているメモリサイズの取得
unsafe.Sizeof
を使います
package main
import (
"fmt"
"unsafe"
)
func main() {
var sliceVal []int
var stringVal string
var interfaceVal interface{}
fmt.Printf("sliceVal:%d, stringVal:%d, interfaceVal:%d\n",
unsafe.Sizeof(sliceVal), unsafe.Sizeof(stringVal), unsafe.Sizeof(interfaceVal))
}
実行結果
$ go run main.go
sliceVal:24, stringVal:16, interfaceVal:16
変数が占めている領域を定義と違う型で再定義する
var num uint = 0x0102030405060708
を[8]uint8
で再定義して表示を試みます。
package main
import (
"fmt"
)
func main() {
var num uint = 0x0102030405060708
reNum := (*[8]uint8)(&num)
fmt.Println(*reNum)
}
$ go run main.go
# command-line-arguments
.\main.go:9:22: cannot convert &num (type *uint) to type *[8]uint8
このままでは型変換できないようです。そこで使うのがunsafe.Pointer
です。
package main
import (
"fmt"
"unsafe"
)
func main() {
var num uint = 0x0102030405060708
reNum := (*[8]uint8)(unsafe.Pointer(&num))
fmt.Println(*reNum)
}
$ go run main.go
[8 7 6 5 4 3 2 1]
無事表示できました。数字の順番がnum
と逆になっているのはCPUがIntel製でリトルエンディアンのためです。
これでも十分ですが、構造体を使った方が後々便利です。さらにfmt.Printf
に変更しています。
package main
import (
"fmt"
"unsafe"
)
type memory struct {
field1 [4]uint8
field2 [2]uint16
}
func main() {
var num uint = 0x0102030405060708
reNum := (*memory)(unsafe.Pointer(&num))
fmt.Printf("%#v\n", *reNum)
}
$ go run main.go
main.memory{field1:[4]uint8{0x8, 0x7, 0x6, 0x5}, field2:[2]uint16{0x304, 0x102}}
スライスの調査
スライスは24バイトなので、とりあえずuint
型3個として中身を見てみます。
package main
import (
"fmt"
"unsafe"
)
type memory struct {
field1 uint
field2 uint
field3 uint
}
func dumpMemory(s []int) {
p := (*memory)(unsafe.Pointer(&s))
fmt.Printf("%+v\n", *p)
fmt.Printf("len: %d, cap: %d\n", len(s), cap(s))
}
func main() {
var sliceVal []int
sliceVal = make([]int, 1, 2)
sliceVal[0] = 12
dumpMemory(sliceVal)
sliceVal = append(sliceVal, 34)
dumpMemory(sliceVal)
sliceVal = append(sliceVal, 56)
dumpMemory(sliceVal)
sliceVal = append(sliceVal, 78)
dumpMemory(sliceVal)
fmt.Println(sliceVal)
}
$ go run main.go
{field1:824633762016 field2:1 field3:2}
len: 1, cap: 2
{field1:824633762016 field2:2 field3:2}
len: 2, cap: 2
{field1:824633795808 field2:3 field3:4}
len: 3, cap: 4
{field1:824633795808 field2:4 field3:4}
len: 4, cap: 4
[12 34 56 78]
結果を見てみるとfield2
が要素数(len)で、field3
が容量(cap)であることがわかる。field1
はアドレスっぽいです。容量(cap)が変わらないときはfield1
の値も変わらないようです。つまり容量が増えると配列用のメモリを新たに割り当てていると推測されます。次にfield1
をuint
型の配列ポインタとして中身を見てみます。プログラムはmemory
とdumpMemory
のみ変更します。
type memory struct {
field1 *[10]uint
field2 uint
field3 uint
}
func dumpMemory(s []int) {
p := (*memory)(unsafe.Pointer(&s))
fmt.Printf("field1: %v\n%+v\n", *p.field1, *p)
fmt.Printf("len: %d, cap: %d\n", len(s), cap(s))
fmt.Println()
}
$ go run main.go
field1: [12 0 0 0 0 0 0 0 0 0]
{field1:0xc000066080 field2:1 field3:2}
len: 1, cap: 2
field1: [12 34 2322222865247988070 0 2322222865247988070 2319389199166484827 1 2 7811852430209544791 101]
{field1:0xc000066080 field2:2 field3:2}
len: 2, cap: 2
field1: [12 34 56 0 0 0 0 0 0 0]
{field1:0xc0000640e0 field2:3 field3:4}
len: 3, cap: 4
field1: [12 34 56 78 0 0 0 0 0 0]
{field1:0xc0000640e0 field2:4 field3:4}
len: 4, cap: 4
[12 34 56 78]
34
を追加したときfield1
のアドレスが変わっていないのにfield1[2]
以降が変わっていますが、スライスで用意した領域はfield1[1]
までなので、他の処理で使われたのだと思います。
結果、スライスは次のような内部構造になっているようです。
相対アドレス | メモリの内容 |
---|---|
0~7バイト目 | 実データの配列があるメモリのポインタ |
8~15バイト目 | 要素数 |
16~23バイト目 | 容量 |
stringの調査
string
のメモリサイズは16バイトなので、uint
型2個として中身を見てみます。
package main
import (
"fmt"
"unsafe"
)
type memory struct {
field1 uint
field2 uint
}
func dumpMemory(s string) {
p := (*memory)(unsafe.Pointer(&s))
fmt.Printf("%+v\n", *p)
}
func main() {
dumpMemory("abcde")
dumpMemory("あいうえお")
}
$ go run main.go
{field1:5003377 field2:5}
{field1:5009736 field2:15}
結果を見ると、field1
はアドレスのようで、field2
は文字数ではなくバイト数のように思われます。そこで、以下のURLを参照してUTF-8の文字コードを調べてみました。ひらがなは3バイトなのでfield2
の値と一致します。
文字 | UTF-8文字コード |
---|---|
a | 61 |
b | 62 |
c | 63 |
d | 64 |
e | 65 |
あ | E38182 |
い | E38184 |
う | E38186 |
え | E38188 |
お | E3818A |
そこで、field1
をbyteの配列として中身を表示してみます。プログラムはmemory
とdumpMemory
のみ変更します。
type memory struct {
field1 *[20]byte
field2 uint
}
func dumpMemory(s string) {
p := (*memory)(unsafe.Pointer(&s))
fmt.Printf("%+v\n", *p)
for i := uint(0); i < p.field2; i++ {
fmt.Printf("%d: %x\n", i, p.field1[i])
}
fmt.Println()
}
$ go run main.go
{field1:0x4c69d1 field2:5}
0: 61
1: 62
2: 63
3: 64
4: 65
{field1:0x4c82af field2:15}
0: e3
1: 81
2: 82
3: e3
4: 81
5: 84
6: e3
7: 81
8: 86
9: e3
10: 81
11: 88
12: e3
13: 81
14: 8a
調べたUTF-8のコードと一致しましたのでfield1
はbyte
配列のポインタで間違いないようです。
結果、stringは次のような内部構造になっているようです。
相対アドレス | メモリの内容 |
---|---|
0~7バイト目 | 実データのbyte配列があるメモリのポインタ |
8~15バイト目 | byte数 |
内部構造が分かったところでstringはイミュータブルで変更不可ですが、無理やり変更してみたくなります。次のような先頭の英文字を変更するプログラムを試みましたがpanicが発生しました。
package main
import (
"fmt"
"unsafe"
)
type memory struct {
field1 *[20]byte
field2 uint
}
func main() {
str := "abcde"
p := (*memory)(unsafe.Pointer(&str))
fmt.Printf("str: %p, fiedl1: %p\n", &str, p.field1)
fmt.Println(str, "\n*************************")
p.field1[0] = '0'
fmt.Println(str)
}
実行結果
$ go run main.go
str: 0xc0000461d0, fiedl1: 0x4c46ec
abcde
*************************
unexpected fault address 0x4c46ec
fatal error: fault
[signal 0xc0000005 code=0x1 addr=0x4c46ec pc=0x491c76]
以下省略
この結果を見るとstr
のアドレスとabcde
が格納されているアドレス(field1
)が随分違うことに気づきます。よってabcde
が格納されているメモリは書き込み不可属性ではないかと推測します。そこでstr
と同じようなアドレスに割り当てられるようにして、再び変更を試みます。 main()
のみ記載します。
func main() {
str0 := "abcd"
str := str0 + "e"
p := (*memory)(unsafe.Pointer(&str))
fmt.Printf("str: %p, fiedl1: %p\n", &str, p.field1)
fmt.Println(str, "\n*************************")
p.field1[0] = '0'
fmt.Println(str)
}
$ go run main.go
str: 0xc00005c1c0, fiedl1: 0xc000070080
abcde
*************************
0bcde
field1
のアドレスが書き込み可能なところに割り当てられたため変更が可能になったようです。
interface{}の調査
メモリサイズは16バイトなので2個のuint
としてメモリダンプします。中に入れるデータとしてもダンプ時に分かるようにint
と同類にします。
package main
import (
"fmt"
"reflect"
"unsafe"
)
type memory struct {
field1 uint
field2 uint
}
func dumpMemory(val interface{}) {
p := (*memory)(unsafe.Pointer(&val))
fmt.Printf("%+v\n", *p)
fmt.Printf("type: %+v\n", reflect.TypeOf(val))
}
func main() {
dumpMemory(123456)
}
$ go run main.go
{field1:4863776 field2:5102552}
type: int
残念ながら123456
は出てきませんでした。データが入っていないときはポインタであると考えます。そこで*[4]uint
型にしてポインタ先のメモリを4個分づつダンプします。
type memory struct {
field1 *[4]uint
field2 *[4]uint
}
func dumpMemory(val interface{}) {
p := (*memory)(unsafe.Pointer(&val))
fmt.Printf("field1: %+v, field2: %+v\n", *p.field1, *p.field2)
fmt.Printf("type: %+v\n", reflect.TypeOf(val))
}
$ go run main.go
field1: [8 0 9369747855051551226 5672240], field2: [123456 536870951 1 4376800]
type: int
field2
の先頭に期待していた数値がでましたのでfield2
が実データのポインタでしょう。val
に入れるデータを変えてもう少し確かめます。 main()
のところで dumpMemory([2]uint{123456, 7890})
としてみます。
$ go run main.go
field1: [16 0 10450611741568835945 5672256], field2: [123456 7890 0 0]
type: [2]uint
field2
は期待通りでした。field1
の先頭は8
から16
になっているのでデータバイト数ではないかと推測できます。
以前、投稿した記事Go言語 構造体をfmt.Printfで表示するときの書式の不思議でreflect.TypeOf
を%#v
で表示したときの構造かも知れないので、reflect.TypeOf
を%#v
と%f
としfield1
も%#v
にします。
func dumpMemory(val interface{}) {
p := (*memory)(unsafe.Pointer(&val))
fmt.Printf("field1: %#v, field2: %+v\n", *p.field1, *p.field2)
fmt.Printf("type: %#v\n : %f\n", reflect.TypeOf(val), reflect.TypeOf(val))
}
$ go run main.go
field1: [4]uint{0x10, 0x0, 0x910808025dbca169, 0x568d40}, field2: [123456 7890 0 0]
type: &reflect.rtype{size:0x10, ptrdata:0x0, hash:0x5dbca169, tflag:0x2, align:0x8, fieldAlign:0x8, kind:0x91, alg:(*reflect.typeAlg)(0x568d40), gcdata:(*uint8)(0x4dcb48), str:8166, ptrToThis:0}
: &{%!f(uintptr=16) %!f(uintptr=0) %!f(uint32=1572643177) %!f(reflect.tflag=2) %!f(uint8=8) %!f(uint8=8) %!f(uint8=145) %!f(*reflect.typeAlg=&{0x402720 0x402f20}) %!f(*uint8=0x4dcb48) %!f(reflect.nameOff=8166) %!f(reflect.typeOff=0)}
field1
とtype
を較べてみると4個中3個(size,ptrdata,alg
)が一致しています。そこでfield1
はtype
であると考えれらます。またfield1
の0x910808025dbca169
がtype
では5個のフィールドになっています。64ビットを5個に分けるには16,16,16,8,8
または32,8,8,8,8
が考えられる。hash
がuint32
、必然的に32,8,8,8,8
の構成になる。alg
が二つの項目を持っていて%f
で16進表示されているのでポインタと考えられます。また%#v
のフォーマットでstr
とptrToThis
が%#v
で10進表示になっているのは符号付きだからだと思います。rtype
は直接使用できないので、自分で11個の項目myType
を定義してみます。
package main
import (
"fmt"
"reflect"
"unsafe"
)
type memory struct {
field1 *myType
field2 *[4]uint
}
type myType struct {
size uintptr
ptrdata uintptr
hash uint32
tflag uint8
align uint8
fieldAlign uint8
kind uint8
alg *[2]*uint
gcdata *uint8
str int
ptrToThis int
}
func dumpMemory(val interface{}) {
p := (*memory)(unsafe.Pointer(&val))
fmt.Printf("field1: %#v\n : %f , field2: %+v\n", *p.field1, *p.field1, *p.field2)
fmt.Printf("type: %#v\n : %f\n", reflect.TypeOf(val), reflect.TypeOf(val))
}
func main() {
dumpMemory([2]uint{123456, 67890})
}
$ go run main.go
field1: main.myType{size:0x10, ptrdata:0x0, hash:0x5dbca169, tflag:0x2, align:0x8, fieldAlign:0x8, kind:0x91, alg:(*[2]*uint)(0x569d40), gcdata:(*uint8)(0x4ddeb8), str:8190, ptrToThis:4870240}
: {%!f(uintptr=16) %!f(uintptr=0) %!f(uint32=1572643177) %!f(uint8=2) %!f(uint8=8) %!f(uint8=8) %!f(uint8=145) %!f(*[2]*uint=&[0x4cf668 0x4cf638]) %!f(*uint8=0x4ddeb8) %!f(int=8190) %!f(int=4870240)} , field2: [123456 67890 0 0]
type: &reflect.rtype{size:0x10, ptrdata:0x0, hash:0x5dbca169, tflag:0x2, align:0x8, fieldAlign:0x8, kind:0x91, alg:(*reflect.typeAlg)(0x569d40), gcdata:(*uint8)(0x4ddeb8), str:8190, ptrToThis:0}
: &{%!f(uintptr=16) %!f(uintptr=0) %!f(uint32=1572643177) %!f(reflect.tflag=2) %!f(uint8=8) %!f(uint8=8) %!f(uint8=145) %!f(*reflect.typeAlg=&{0x402720 0x402f20}) %!f(*uint8=0x4ddeb8) %!f(reflect.nameOff=8190) %!f(reflect.typeOff=0)}
alg
の二つのポインタが違っています。ポインタだと思うのでデータポインタでなく関数のポインタにしてみます。また、ptrToThis
の値が違うのでstr
とptrToThis
をint32
にしてみます。
type myType struct {
size uintptr
ptrdata uintptr
hash uint32
tflag uint8
align uint8
fieldAlign uint8
kind uint8
alg *[2]func()
gcdata *uint8
str int32
ptrToThis int32
}
$ go run main.go
field1: main.myType{size:0x10, ptrdata:0x0, hash:0x5dbca169, tflag:0x2, align:0x8, fieldAlign:0x8, kind:0x91, alg:(*[2]func())(0x569d40), gcdata:(*uint8)(0x4ddeb8), str:8190, ptrToThis:0}
: {%!f(uintptr=16) %!f(uintptr=0) %!f(uint32=1572643177) %!f(uint8=2) %!f(uint8=8) %!f(uint8=8) %!f(uint8=145) %!f(*[2]func()=&[0x402720 0x402f20]) %!f(*uint8=0x4ddeb8) %!f(int32=8190) %!f(int32=0)} , field2: [123456 67890 0 0]
type: &reflect.rtype{size:0x10, ptrdata:0x0, hash:0x5dbca169, tflag:0x2, align:0x8, fieldAlign:0x8, kind:0x91, alg:(*reflect.typeAlg)(0x569d40), gcdata:(*uint8)(0x4ddeb8), str:8190, ptrToThis:0}
: &{%!f(uintptr=16) %!f(uintptr=0) %!f(uint32=1572643177) %!f(reflect.tflag=2) %!f(uint8=8) %!f(uint8=8) %!f(uint8=145) %!f(*reflect.typeAlg=&{0x402720 0x402f20}) %!f(*uint8=0x4ddeb8) %!f(reflect.nameOff=8190) %!f(reflect.typeOff=0)}
ここで完全に一致しました。alg
が関数のポインタとは分かりましたが、関数の引数や戻り値の型などまでは分かりません。私がプログラミングで調べられるのはここまでです。
結論としてfield1
はType
の構造体のポインタでType
の先頭項目はデータのバイト数です。field2
は実データがある領域のポインタです。最後のほうはreflect.rtype
の構造の調査になってしまいました。
相対アドレス | メモリの内容 |
---|---|
0~7バイト目 | データのタイプ情報(reflect.rtype)のポインタ |
8~15バイト目 | 実データが格納されているポインタ |
Appendix
Goソースを調べてみた
string とスライス
スライスとstringの内部構造についてGo言語のソースを探してみましたら、それらしいのが見つかりました。ソース名はreflect/value.go
です。
// StringHeader is the runtime representation of a string.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type StringHeader struct {
Data uintptr
Len int
}
// stringHeader is a safe version of StringHeader used within this package.
type stringHeader struct {
Data unsafe.Pointer
Len int
}
// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
// sliceHeader is a safe version of SliceHeader used within this package.
type sliceHeader struct {
Data unsafe.Pointer
Len int
Cap int
}
interface{}
Go言語のソースからreflect/value.go
とreflect/type.go
に下記のような構造体を見つけました。
memory
がemptyInterface
に対応して、 myType
がrtype
に対応しているようです。
// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}
// rtype is the common implementation of most values.
// It is embedded in other struct types.
//
// rtype must be kept in sync with ../runtime/type.go:/^type._type.
type rtype struct {
size uintptr
ptrdata uintptr // number of bytes in the type that can contain pointers
hash uint32 // hash of type; avoids computation in hash tables
tflag tflag // extra type information flags
align uint8 // alignment of variable with this type
fieldAlign uint8 // alignment of struct field with this type
kind uint8 // enumeration for C
alg *typeAlg // algorithm table
gcdata *byte // garbage collection data
str nameOff // string form
ptrToThis typeOff // type for pointer to this type, may be zero
}
tflag
はuint8
で、nameOff
とtypeOff
はint32
です。