0%

《十万个为什么》为什么对象设置为nil还能调用其方法

分析

看下面案例,分析一下会发生什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

type Test struct {
abc int
}

func (t *Test) test() {
fmt.Println(123)
}

func main() {
a := &Test{
abc: 1,
}
a = nil
a.test()
}

如果你认为会发生空指针异常,你就错了,答案是会打印123

分析下汇编代码,同样是先go build --gcflags="-l -N" ./cmd/main/,然后go tool objdump -s "main.main" main

下面是main方法的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
main.go:13            0x109d000               65488b0c2530000000      MOVQ GS:0x30, CX                        
main.go:13 0x109d009 483b6110 CMPQ 0x10(CX), SP
main.go:13 0x109d00d 7651 JBE 0x109d060 // 这里往上都是栈检查
main.go:13 0x109d00f 4883ec28 SUBQ $0x28, SP // 开辟0x28空间的栈
main.go:13 0x109d013 48896c2420 MOVQ BP, 0x20(SP) // 备份main调用者的栈基
main.go:13 0x109d018 488d6c2420 LEAQ 0x20(SP), BP // 栈基指向0x20(SP)位置
main.go:14 0x109d01d 48c744240800000000 MOVQ $0x0, 0x8(SP) // 值0放入栈0x8的位置
main.go:14 0x109d026 488d442408 LEAQ 0x8(SP), AX // AX指向0x8(SP)位置
main.go:14 0x109d02b 4889442418 MOVQ AX, 0x18(SP) // 指向0x8位置的指针值放入0x18(SP)位置
main.go:14 0x109d030 8400 TESTB AL, 0(AX)
main.go:15 0x109d032 48c744240801000000 MOVQ $0x1, 0x8(SP) // 1放入0x8(SP)中
main.go:14 0x109d03b 4889442410 MOVQ AX, 0x10(SP) // 指向0x8位置的指针值放入0x10(SP)位置
main.go:17 0x109d040 48c744241000000000 MOVQ $0x0, 0x10(SP) // 0放入0x10(SP)位置
main.go:18 0x109d049 48c7042400000000 MOVQ $0x0, 0(SP) // 0放入0(SP)位置作为参数
main.go:18 0x109d051 e80affffff CALL main.(*Test).test(SB)
main.go:19 0x109d056 488b6c2420 MOVQ 0x20(SP), BP // 恢复栈基
main.go:19 0x109d05b 4883c428 ADDQ $0x28, SP // 清空栈帧。栈顶SP重新指向0x28
main.go:19 0x109d05f c3 RET // -0x8处的返回地址放入EIP
main.go:13 0x109d060 e86bc3fbff CALL runtime.morestack_noctxt(SB)
main.go:13 0x109d065 eb99 JMP main.main(SB)

下面是main方法的栈帧

1
2
3
4
5
6
7
8
9
10
11
12
13
0x28(40)                              
main的调用者的栈基
0x20(32) <--------- main的栈基BP指向位置
指向0x8位置的指针
0x18(24)
指向0x8位置的指针 --> 0x0 (这里就是a变量的值)
0x10(16)
0x0 --> 0x1 (这里就是Test实例的实际数据)
0x8(8) <---------- AX
0x0
0x0(0) <---------- main的栈顶SP指向位置
test执行完后的返回地址
-0x8(-8)

下面是test函数的汇编代码

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.go:9             0x109cf60               65488b0c2530000000      MOVQ GS:0x30, CX                        
main.go:9 0x109cf69 483b6110 CMPQ 0x10(CX), SP
main.go:9 0x109cf6d 767a JBE 0x109cfe9 // 这里往上都是栈检查
main.go:9 0x109cf6f 4883ec68 SUBQ $0x68, SP // 开辟栈空间
main.go:9 0x109cf73 48896c2460 MOVQ BP, 0x60(SP) // 备份栈基
main.go:9 0x109cf78 488d6c2460 LEAQ 0x60(SP), BP // 修改栈基指向
main.go:10 0x109cf7d 0f57c0 XORPS X0, X0 // 这里往下都是fmt打印
main.go:10 0x109cf80 0f11442438 MOVUPS X0, 0x38(SP)
main.go:10 0x109cf85 488d442438 LEAQ 0x38(SP), AX
main.go:10 0x109cf8a 4889442430 MOVQ AX, 0x30(SP)
main.go:10 0x109cf8f 8400 TESTB AL, 0(AX)
main.go:10 0x109cf91 488d0d68db0000 LEAQ type.*+55936(SB), CX
main.go:10 0x109cf98 48894c2438 MOVQ CX, 0x38(SP)
main.go:10 0x109cf9d 488d0dacb20400 LEAQ $f64.fffffffffffffffe+8(SB), CX
main.go:10 0x109cfa4 48894c2440 MOVQ CX, 0x40(SP)
main.go:10 0x109cfa9 8400 TESTB AL, 0(AX)
main.go:10 0x109cfab eb00 JMP 0x109cfad
main.go:10 0x109cfad 4889442448 MOVQ AX, 0x48(SP)
main.go:10 0x109cfb2 48c744245001000000 MOVQ $0x1, 0x50(SP)
main.go:10 0x109cfbb 48c744245801000000 MOVQ $0x1, 0x58(SP)
main.go:10 0x109cfc4 48890424 MOVQ AX, 0(SP)
main.go:10 0x109cfc8 48c744240801000000 MOVQ $0x1, 0x8(SP)
main.go:10 0x109cfd1 48c744241001000000 MOVQ $0x1, 0x10(SP)
main.go:10 0x109cfda e8e199ffff CALL fmt.Println(SB)
main.go:11 0x109cfdf 488b6c2460 MOVQ 0x60(SP), BP // 恢复栈基
main.go:11 0x109cfe4 4883c468 ADDQ $0x68, SP // 清空栈帧
main.go:11 0x109cfe8 c3 RET // EIP切到fmt.Println下一行
main.go:9 0x109cfe9 e8e2c3fbff CALL runtime.morestack_noctxt(SB)
main.go:9 0x109cfee e96dffffff JMP main.(*Test).test(SB)

从汇编代码中可以分析到,整个过程没有出现任何问题,所以自然不会报错

那如果改成下面这样呢?(仅仅改了打印的内容)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

type Test struct {
abc int
}

func (t *Test) test() {
fmt.Println(t.abc)
}

func main() {
a := &Test{
abc: 1,
}
a = nil
a.test()
}

运行后会发现报错了,空指针异常,下面还是通过汇编分析一下

main方法内容没有改动,只需看test函数的汇编代码

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
main.go:9             0x109cf60               65488b0c2530000000      MOVQ GS:0x30, CX                        
main.go:9 0x109cf69 483b6110 CMPQ 0x10(CX), SP
main.go:9 0x109cf6d 0f869d000000 JBE 0x109d010
main.go:9 0x109cf73 4883ec78 SUBQ $0x78, SP
main.go:9 0x109cf77 48896c2470 MOVQ BP, 0x70(SP)
main.go:9 0x109cf7c 488d6c2470 LEAQ 0x70(SP), BP
main.go:10 0x109cf81 488b842480000000 MOVQ 0x80(SP), AX // 将test函数的第一个参数,也就是Test实例指针t放入AX。AX中的值是0(这里往下都是fmt打印)
main.go:10 0x109cf89 8400 TESTB AL, 0(AX)
main.go:10 0x109cf8b 488b00 MOVQ 0(AX), AX // 将t的0位置数据,也就是Test结构第一个元素abc的值放入AX。因为AX是0,0(AX)就会触发非法内存访问,操作系统通过发送kill信号杀死进程,进程捕捉了信号,报出空指针异常信息
main.go:10 0x109cf8e 4889442430 MOVQ AX, 0x30(SP) // abc的值放入0x30(SP)位置
main.go:10 0x109cf93 48890424 MOVQ AX, 0(SP) // abc的值放入0(SP)位置,作为runtime.convT64的参数
main.go:10 0x109cf97 e854c0f6ff CALL runtime.convT64(SB)
main.go:10 0x109cf9c 488b442408 MOVQ 0x8(SP), AX // runtime.convT64的返回值放入AX
main.go:10 0x109cfa1 4889442440 MOVQ AX, 0x40(SP)
main.go:10 0x109cfa6 0f57c0 XORPS X0, X0
main.go:10 0x109cfa9 0f11442448 MOVUPS X0, 0x48(SP)
main.go:10 0x109cfae 488d442448 LEAQ 0x48(SP), AX
main.go:10 0x109cfb3 4889442438 MOVQ AX, 0x38(SP)
main.go:10 0x109cfb8 8400 TESTB AL, 0(AX)
main.go:10 0x109cfba 488b4c2440 MOVQ 0x40(SP), CX
main.go:10 0x109cfbf 488d155adb0000 LEAQ type.*+55936(SB), DX
main.go:10 0x109cfc6 4889542448 MOVQ DX, 0x48(SP)
main.go:10 0x109cfcb 48894c2450 MOVQ CX, 0x50(SP)
main.go:10 0x109cfd0 8400 TESTB AL, 0(AX)
main.go:10 0x109cfd2 eb00 JMP 0x109cfd4
main.go:10 0x109cfd4 4889442458 MOVQ AX, 0x58(SP)
main.go:10 0x109cfd9 48c744246001000000 MOVQ $0x1, 0x60(SP)
main.go:10 0x109cfe2 48c744246801000000 MOVQ $0x1, 0x68(SP)
main.go:10 0x109cfeb 48890424 MOVQ AX, 0(SP)
main.go:10 0x109cfef 48c744240801000000 MOVQ $0x1, 0x8(SP)
main.go:10 0x109cff8 48c744241001000000 MOVQ $0x1, 0x10(SP)
main.go:10 0x109d001 e8ba99ffff CALL fmt.Println(SB)
main.go:11 0x109d006 488b6c2470 MOVQ 0x70(SP), BP
main.go:11 0x109d00b 4883c478 ADDQ $0x78, SP
main.go:11 0x109d00f c3 RET
main.go:9 0x109d010 e8bbc3fbff CALL runtime.morestack_noctxt(SB)
main.go:9 0x109d015 e946ffffff JMP main.(*Test).test(SB)

从上面分析可以看出来了,空指针异常是因为内存页的非法访问,导致操作系统通过kill发送SIGBUS信号(当出现某种类型的内存故障时,会产生此种信号)给进程

而进程安装了信号处理函数,信号处理函数发现是SIGBUS信号,就会报出空指针异常的错误信息,并退出进程

总结

  1. 任何函数都是方法,只不过函数的第一个参数是struct实例,所以即使struct实例是nil,函数仍然可以被调用,不会发生空指针异常
  2. 空指针异常是因为发生了非法内存访问,操作系统发现后发送SIGBUS信号给进程,进程处理SIGBUS信号报出空指针异常



微信关注我,及时接收最新技术文章