朱晓峰

一只生之无趣死之乏味的丧家之犬

0%

Golang panic疑难杂症(二)

panic日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x512f5d]

goroutine 48438156 [running]:
encoding/json.(*encodeState).marshal.func1(0xc009decb40)
/go/src/encoding/json/encode.go:326 +0x9a
panic(0xd41f60, 0x187d170)
/go/src/runtime/panic.go:967 +0x15d
encoding/json.(*encodeState).string(0xc004eba100, 0x0, 0x7, 0x1)
/go/src/encoding/json/encode.go:1003 +0x5d
encoding/json.stringEncoder(0xc004eba100, 0xce54e0, 0xc008bfeea0, 0x198, 0x100)
/go/src/encoding/json/encode.go:644 +0x358
encoding/json.structEncoder.encode(0xc00451c000, 0x13, 0x21, 0xc0058d99e0, 0xc004eba100, 0xe29520, 0xc008bfeea0, 0x199, 0xd30100)
/go/src/encoding/json/encode.go:758 +0x2bb
encoding/json.ptrEncoder.encode(0xc0058d9a70, 0xc004eba100, 0xd26880, 0xc008bfeea0, 0x16, 0xd20100)
/go/src/encoding/json/encode.go:914 +0x116
encoding/json.(*encodeState).reflectValue(0xc004eba100, 0xd26880, 0xc008bfeea0, 0x16, 0xc009de0100)
/go/src/encoding/json/encode.go:358 +0x82
encoding/json.(*encodeState).marshal(0xc004eba100, 0xd26880, 0xc008bfeea0, 0x450100, 0x0, 0x0)
/go/src/encoding/json/encode.go:330 +0xf0
encoding/json.Marshal(0xd26880, 0xc008bfeea0, 0x18cf180, 0x8, 0xc0029067a8, 0xc002038c40, 0x15)
/go/src/encoding/json/encode.go:161 +0x52
...

可以看到,是json encode string的时候发生的panic,这种情况比较少见。

重点是这一行日志:

encoding/json.(*encodeState).string(0xc004eba100, 0x0, 0x7, 0x1)

string()函数的入参有0x0和0x7

实际上这里的0x0是指字符串变量指向内容的指针为0,表示长度的变量为0x7

原理

string类型底层实现是一个指向内容的指针和一个表示长度的int

1
2
3
4
5
6
7
8
9
10
// 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
}

对字符串的赋值,分为两部分,所以在go里对字符串进行并发读写的话,有可能会导致指向的内容和长度不匹配,导致一些奇怪的问题。

比如两个协程同时并发读写同一个字符串变量,一个赋值为”123”,另一个赋值为”123456”,该字符串有可能为”123”,但长度是6。

但是就算并发读写,Data指针指向的一直是正常地址,只是长度不一定匹配,又怎么会导致nil pointer的呢?

其实,在go里,有一种特殊优化,如果你把字符串赋值为””,即空字符串的话,Data指针是0,也即空指针。

我们来验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
"fmt"
"time"
)


type AudioClips struct {
Param *TableStruct
}

type TableStruct struct {
RequestId string
Type string
Organization string
BtId string
AppId string
TokenId string
Channel string
}


func f(org, serviceId, appId, channel string) {
curTime := time.Now().Format(layout)
fmt.Sprintf("%s@%s@%s@%s@%s@%s", curTime, org, serviceId, appId, channel, "length")
}

var layout string

func main() {
a := AudioClips{
Param: &TableStruct{
AppId: "",
Organization: "org",
Channel: "channel",
},
}

go func() {
for {
f(a.Param.Organization, "serviceId", a.Param.AppId, a.Param.Channel)
}
}()

for {
go func() {a.Param.AppId = ""}()
time.Sleep(10*time.Nanosecond)
go func() {a.Param.AppId = "default"}()
time.Sleep(10*time.Nanosecond)
}
}

运行后发生panic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ go run main.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0xe570a2]

goroutine 34 [running]:
fmt.(*buffer).writeString(...)
C:/Program Files/Go/src/fmt/print.go:82
fmt.(*fmt).padString(0xc0000202b0, 0x0, 0x7)
C:/Program Files/Go/src/fmt/format.go:110 +0x98
fmt.(*fmt).fmtS(0xc0000202b0, 0x0, 0x7)
C:/Program Files/Go/src/fmt/format.go:359 +0x68
fmt.(*pp).fmtString(0xc000020270, 0x0, 0x7, 0x21700000073)
C:/Program Files/Go/src/fmt/print.go:446 +0x1d2
fmt.(*pp).printArg(0xc000020270, 0xead6c0, 0xc0003de2b0, 0x73)
C:/Program Files/Go/src/fmt/print.go:694 +0x875
fmt.(*pp).doPrintf(0xc000020270, 0xec9c4d, 0x11, 0xc000097f18, 0x6, 0x6)
C:/Program Files/Go/src/fmt/print.go:1026 +0x168
fmt.Sprintf(0xec9c4d, 0x11, 0xc000097f18, 0x6, 0x6, 0x0, 0x0)
C:/Program Files/Go/src/fmt/print.go:219 +0x6d
main.f(0xec799a, 0x3, 0xec8574, 0x9, 0x0, 0x7, 0xec7fd9, 0x7)
C:/Projects/test/11/main.go:43 +0x1de
main.main.func1(0xc000194000)
C:/Projects/test/11/main.go:59 +0x8a
created by main.main
C:/Projects/test/11/main.go:57 +0x91
exit status 2

看下汇编代码,略有精简

go tool compile -S -N -l main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
...
"".main.func2 STEXT size=92 args=0x8 locals=0x8
0x0000 00000 (main.go:47) TEXT "".main.func2(SB), ABIInternal, $8-8
0x0000 00000 (main.go:47) MOVQ (TLS), CX
0x0009 00009 (main.go:47) CMPQ SP, 16(CX)
0x000d 00013 (main.go:47) PCDATA $0, $-2
0x000d 00013 (main.go:47) JLS 85
0x000f 00015 (main.go:47) PCDATA $0, $-1
0x000f 00015 (main.go:47) SUBQ $8, SP
0x0013 00019 (main.go:47) MOVQ BP, (SP)
0x0017 00023 (main.go:47) LEAQ (SP), BP
0x001b 00027 (main.go:47) MOVQ "".a+16(SP), CX
0x0020 00032 (main.go:47) TESTB AL, (CX)
0x0022 00034 (main.go:47) MOVQ $0, 72(CX)
0x002a 00042 (main.go:47) LEAQ 64(CX), DI
0x002e 00046 (main.go:47) CMPL runtime.writeBarrier(SB), $0
0x0035 00053 (main.go:47) JEQ 57
0x0037 00055 (main.go:47) JMP 76
0x0039 00057 (main.go:47) MOVQ $0, 64(CX)
0x0041 00065 (main.go:47) JMP 67
0x0043 00067 (main.go:47) PCDATA $0, $-1
0x0043 00067 (main.go:47) PCDATA $1, $-1
0x0043 00067 (main.go:47) MOVQ (SP), BP
0x0047 00071 (main.go:47) ADDQ $8, SP
0x004b 00075 (main.go:47) RET
...

MOVQ $0, 72(CX) 表示把立即数0存入CX地址+72,即把长度0存入Len

MOVQ $0, 64(CX) 表示把立即数0存入CX地址+64,即指向字符串内容的指针值为0,也就是nil pointer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
...
"".main.func3 STEXT size=100 args=0x8 locals=0x8
0x0000 00000 (main.go:49) TEXT "".main.func3(SB), ABIInternal, $8-8
0x0000 00000 (main.go:49) MOVQ (TLS), CX
0x0009 00009 (main.go:49) CMPQ SP, 16(CX)
0x000d 00013 (main.go:49) PCDATA $0, $-2
0x000d 00013 (main.go:49) JLS 93
0x000f 00015 (main.go:49) PCDATA $0, $-1
0x000f 00015 (main.go:49) SUBQ $8, SP
0x0013 00019 (main.go:49) MOVQ BP, (SP)
0x0017 00023 (main.go:49) LEAQ (SP), BP
0x001b 00027 (main.go:49) MOVQ "".a+16(SP), CX
0x0020 00032 (main.go:49) TESTB AL, (CX)
0x0022 00034 (main.go:49) MOVQ $7, 72(CX)
0x002a 00042 (main.go:49) LEAQ 64(CX), DI
0x002e 00046 (main.go:49) PCDATA $0, $-2
0x002e 00046 (main.go:49) PCDATA $1, $-2
0x002e 00046 (main.go:49) CMPL runtime.writeBarrier(SB), $0
0x0035 00053 (main.go:49) JEQ 57
0x0037 00055 (main.go:49) JMP 79
0x0039 00057 (main.go:49) LEAQ go.string."default"(SB), AX
0x0040 00064 (main.go:49) MOVQ AX, 64(CX)
0x0044 00068 (main.go:49) JMP 70
0x0046 00070 (main.go:49) PCDATA $0, $-1
0x0046 00070 (main.go:49) PCDATA $1, $-1
0x0046 00070 (main.go:49) MOVQ (SP), BP
0x004a 00074 (main.go:49) ADDQ $8, SP
0x004e 00078 (main.go:49) RET
...

MOVQ $7, 72(CX) 表示把立即数7存入CX地址+72,即把长度0存入Len

1
2
LEAQ    go.string."default"(SB), AX
MOVQ AX, 64(CX)

表示把指向”default”的地址存入AX,再把AX存入64(CX),即把指向字符串内容的指针赋值为”default”的地址

所以在json encode这个字符串的时候,可能会发生字符串有长度,但指向内容的指针是空指针,

可以通过len(str)>0这样的判断,但真正去取值的时候就发生了nil pointer panic。

更进一步

所以可见,如果某个字符串变量存储了很多的内容,可以通过赋值为空字符串进行释放。

那么问题又来了,如果按上面的分析,赋值为空字符串,指针就会为空,那为什么如下代码没有发生panic

1
2
a := ""
p := &a

其实编译器会判断到如果这个空字符串在后续是有取指针操作的话,加上如下汇编代码:

1
2
3
4
5
6
7
0x003c 00060 (main.go:9)	MOVQ	"".&p+24(SP), DI
0x0041 00065 (main.go:9) MOVQ "".&a+16(SP), AX
0x0046 00070 (main.go:9) CMPL runtime.writeBarrier(SB), $0
0x004d 00077 (main.go:9) JEQ 81
0x004f 00079 (main.go:9) JMP 95
0x0051 00081 (main.go:9) MOVQ AX, (DI)
0x0054 00084 (main.go:9) JMP 86

以保证可以正常取到指针