内存对齐

  1. C语言函数调用中,通过栈传递的参数需要对齐到平台的位宽。
  2. 假如通过栈传递4个char类型的参数,GCC生成的 32 位程序需要 16 字节空间,64 位程序需要 32 字节栈空间。
  3. 如果传递大量参数,则这种对齐方式会存在很大的栈空间浪费。
  4. Go语言函数栈帧中返回值和参数的对齐方式与 struct 类似,对于有返回值和参数的函数,可以把所有返回值和所有参数等价成两个 struct
    • 一个【返回值 struct】 和一个【参数 struct】。
  5. 因为内存对齐方式更加紧凑,所以在支持大量参数和返回值时能够做到较高的栈空间利用率。
  6. 通过如下示例可以验证函数参数和返回值的对齐方式与 struct 成员的对齐方式是一致的,代码如下:
    • 以上的描述都是在函数通过传递参数和返回值的基础上的。在go1.18后版本中采用了寄存器传参。
 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
package main

type args struct {
    a int8    // 1
    b int64 // 8
    c int32 // 4
    d int16 // 2
}

//go:noinline
func f1(a args) (r args) {
    println(&r.d, &r.c, &r.b, &r.a, &a.d, &a.c, &a.b, &a.a)
    return
}

//go:noinline
func f2(aa int8, ab int64, ac int32, ad int16) (ra int8, rb int64, rc int32, rd int16) {
    println(&rd, &rc, &rb, &ra, &ad, &ac, &ab, &aa)
    return
}

func main() {
    f1(args{})
    // 0xc000034744     r.d
    // 0xc000034740     r.c
    // 0xc000034738     r.b
    // 0xc000034730     r.a
    // 0xc00003476c     a.d
    // 0xc000034768     a.c
    // 0xc000034760     a.b
    // 0xc000034758     a.a
    
    f2(0, 0, 0, 0)
    // 0xc00003473a     rd
    // 0xc00003473c     rc
    // 0xc000034740     rb
    // 0xc000034739     ra
    // 0xc00003476c     ad
    // 0xc000034768     ac
    // 0xc000034760     ab
    // 0xc000034758     aa
}

汇编代码
 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
TEXT main.main(SB) /mnt/hgfs/workspace/helium/main.go
func main() {
  0x4551e0        493b6610            CMPQ 0x10(R14), SP    
  0x4551e4        7657                JBE 0x45523d        
  0x4551e6        4883ec38            SUBQ $0x38, SP        
  0x4551ea        48896c2430          MOVQ BP, 0x30(SP)    
  0x4551ef        488d6c2430          LEAQ 0x30(SP), BP    
    f1(args{})
  0x4551f4        c644241800          MOVB $0x0, 0x18(SP)    # a.a
  0x4551f9        48c744242000000000  MOVQ $0x0, 0x20(SP)    # a.b
  0x455202        c744242800000000    MOVL $0x0, 0x28(SP)    # a.c
  0x45520a        66c744242c0000      MOVW $0x0, 0x2c(SP)    # a.d
  0x455211        0fb6442418          MOVZX 0x18(SP), AX     # a.a AX    
  0x455216        488b5c2420          MOVQ 0x20(SP), BX      # a.b BX
  0x45521b        8b4c2428            MOVL 0x28(SP), CX      # a.c CX
  0x45521f        31ff                XORL DI, DI            # a.d DI
  0x455221        e83a000000          CALL main.f1(SB)    
    f2(0, 0, 0, 0)
  0x455226        31c0                XORL AX, AX        # aa    AX
  0x455228        31db                XORL BX, BX        # ab    BX
  0x45522a        31c9                XORL CX, CX        # ac    CX
  0x45522c        31ff                XORL DI, DI        # ad    DI
  0x45522e        e8ad010000          CALL main.f2(SB)    
}
  0x455233        488b6c2430          MOVQ 0x30(SP), BP    
  0x455238        4883c438            ADDQ $0x38, SP        
  0x45523c        c3                  RET            
func main() {
  0x45523d        0f1f00              NOPL 0(AX)                
0x455240          e81bcdffff          CALL runtime.morestack_noctxt.abi0(SB)    
  0x455245        eb99                JMP main.main(SB)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
TEXT main.f1(SB) /mnt/hgfs/workspace/helium/main.go
func f1(a args) (r args) {
  0x4551e0        493b6610            CMPQ 0x10(R14), SP    
  0x4551e4        0f8642010000        JBE 0x45532c        
  0x4551ea        4883ec68            SUBQ $0x68, SP        
  0x4551ee        48896c2460          MOVQ BP, 0x60(SP)    
  0x4551f3        488d6c2460          LEAQ 0x60(SP), BP    
  # 参数分配栈大小空间
  0x4551f8        88442470            MOVB AL, 0x70(SP)    # a.a
  0x4551fc        48895c2478          MOVQ BX, 0x78(SP)    # a.b
  0x455201        898c2480000000      MOVL CX, 0x80(SP)    # a.c
  0x455208        6689bc2484000000    MOVW DI, 0x84(SP)    # a.d
  # 返回值分配栈大小空间
  0x455210        c644240800          MOVB $0x0, 0x8(SP)   # r.a
  0x455215        48c744241000000000  MOVQ $0x0, 0x10(SP)  # r.b
  0x45521e        c744241800000000    MOVL $0x0, 0x18(SP)  # r.c
  0x455226        66c744241c0000      MOVW $0x0, 0x1c(SP)  # r.d
    println(&r.d, &r.c, &r.b, &r.a, &a.d, &a.c, &a.b, &a.a)
  ... ...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
TEXT main.f2(SB) /mnt/hgfs/workspace/helium/main.go
func f2(aa int8, ab int64, ac int32, ad int16) (ra int8, rb int64, rc int32, rd int16) {
  0x455360        493b6610            CMPQ 0x10(R14), SP    
  0x455364        0f8631010000        JBE 0x45549b        
  0x45536a        4883ec60            SUBQ $0x60, SP        
  0x45536e        48896c2458          MOVQ BP, 0x58(SP)    
  0x455373        488d6c2458          LEAQ 0x58(SP), BP    
  # 参数传参,使用寄存器,但是参数空间还是没有优化
  0x455378        88442468            MOVB AL, 0x68(SP)    # aa
  0x45537c        48895c2470          MOVQ BX, 0x70(SP)    # ab
  0x455381        894c2478            MOVL CX, 0x78(SP)    # ac
  0x455385        66897c247c          MOVW DI, 0x7c(SP)    # ad
  # 返回值分配栈大小空间
  0x45538a        c644240900          MOVB $0x0, 0x9(SP)   # ra
  0x45538f        48c744241000000000  MOVQ $0x0, 0x10(SP)  # rb
  0x455398        c744240c00000000    MOVL $0x0, 0xc(SP)   # rc
  0x4553a0        66c744240a0000      MOVW $0x0, 0xa(SP)   # rd
    println(&rd, &rc, &rb, &ra, &ad, &ac, &ab, &aa)
  ... ...

逃逸分析

什么是逃逸分析

局部变量地址作为返回值(1)

  1. 在解释逃逸分析之前,先来思考一个场景,如果一个函数把自已栈帧上某个局部变量的地址作为返回值返回。
  2. 会有什么问题?示例代码如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

func main() {
    println(*newInt())
}

//go:noinline
func newInt() *int {
    var a int
    return &a
}
  1. 前面对函数栈布局的讲解 newInt() 函数的局部变量a应该分配在函数栈的 locals 区间。
  2. newInt()函数返回后,它的栈随即销毁,返回的变量a的地址就会变成一个悬挂指针,caller 中对该地址进行的所有读写都是不合法的,会造成程序逻辑错误甚至崩溃。
  3. 事实是这样的吗?上述分析有个前提条件,即变量 a 被分配在栈上。假如编译器能够检测到这种模式,而自动把变量 a 改为堆分配,就不存在上述问题了。
  4. 反编译 newInt() 函数,看一下结果,代码如下:
    1. 重点关注上述汇编代码中 runtime.newobject() 函数调用,该函数是 Go 语言内置函数 new() 的具体实现,用来在运行阶段分配单个对象。
    2. CALL 指令 AX 寄存器就是 *int 类型,也是返回的值。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
TEXT main.newInt(SB) /mnt/hgfs/workspace/helium/main.go
func newInt() *int {
  0x455240        493b6610            CMPQ 0x10(R14), SP     # 栈增长判断
  0x455244        7643                JBE 0x455289        
  0x455246        4883ec28            SUBQ $0x28, SP        
  0x45524a        48896c2420          MOVQ BP, 0x20(SP)    
  0x45524f        488d6c2420          LEAQ 0x20(SP), BP    
  0x455254        48c744241000000000  MOVQ $0x0, 0x10(SP)    # *int 临时返回值空间
    var a int
  0x45525d        488d05bc4a0000      LEAQ 0x4abc(IP), AX    # type.int 元类型
  # func newobject(typ *_type) unsafe.Pointer
  # 这里调用 newobject 函数进行了堆分配
  0x455264        e8d75cfbff          CALL runtime.newobject(SB)  # AX=*int 类型
  0x455269        4889442418          MOVQ AX, 0x18(SP)        
  0x45526e        48c70000000000      MOVQ $0x0, 0(AX)     # *a=0
    return &a
  0x455275        488b442418          MOVQ 0x18(SP), AX    
  0x45527a        4889442410          MOVQ AX, 0x10(SP)    # 返回值 AX
  0x45527f        488b6c2420          MOVQ 0x20(SP), BP    
  0x455284        4883c428            ADDQ $0x28, SP        
  0x455288        c3                  RET            
func newInt() *int {
  0x455289        e8d2ccffff          CALL runtime.morestack_noctxt.abi0(SB)    
  0x45528e        ebb0                JMP main.newInt(SB)

局部变量地址作为返回值(2)

  1. 如果把 newInt() 函数中的取地址运算改成使用内置函数 new(),效果也是一样的,代码如下:
1
2
3
4
//go:noinline
func newInt() *int {
    return new(int)
}
  1. 相关汇编:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ go build -gcflags="-N -l" -o ./h1 myzx.cn/helium
$ go tool objdump -S -s '^main.newInt$' ./h1 
TEXT main.newInt(SB) /mnt/hgfs/workspace/helium/main.go
func newInt() *int {
  0x455240        493b6610              CMPQ 0x10(R14), SP    
  0x455244        7637                  JBE 0x45527d        
  0x455246        4883ec28              SUBQ $0x28, SP        
  0x45524a        48896c2420            MOVQ BP, 0x20(SP)    
  0x45524f        488d6c2420            LEAQ 0x20(SP), BP    
  0x455254        48c744241000000000    MOVQ $0x0, 0x10(SP)    # *int 返回值空间
    return new(int)
    # func newobject(typ *_type) unsafe.Pointer
  0x45525d        488d05bc4a0000        LEAQ 0x4abc(IP), AX    # type.int 元类型
  0x455264        e8d75cfbff            CALL runtime.newobject(SB)    # AX=*int
  0x455269        4889442418            MOVQ AX, 0x18(SP)        
  0x45526e        4889442410            MOVQ AX, 0x10(SP)        
  0x455273        488b6c2420            MOVQ 0x20(SP), BP        
  0x455278        4883c428              ADDQ $0x28, SP            
  0x45527c        c3                    RET                
func newInt() *int {
  0x45527d        0f1f00                NOPL 0(AX)                
  0x455280        e8dbccffff            CALL runtime.morestack_noctxt.abi0(SB)    
  0x455285        ebb9                  JMP main.newInt(SB)
  1. 当函数局部变量的生命周期超过函数栈的生命周期时,编译器把该局部变量由分配改为分配,即变量从栈上逃逸到堆上。

不逃逸分析

只要使用了new()函数就会造成堆分配?

  1. 前面逃逸分析代码示例中将函数的某个局部变量的地址作为返回值返回,或者通过内置函数 new() 动态分配变量并返回其地址。
  2. 其中内置函数new()有着非常明显的堆分配的含义,是不是只要使用了new()函数就会造成堆分配呢?
  3. 进一步猜想,如果对局部变量进行取地址操作会被转换为new()函数调用,那就不会进行所谓的逃逸分析了。
  4. 先验证new()函数与堆分配是否有必然关系,代码如下:
1
2
3
4
5
//go:noinline
func New() int {
    p := new(int)
    return *p
}
  1. 反编译new()函数,得到的汇编代码如下:
    1. 即便代码中使用了new()函数,只要变量的生命周期没有超过过当前函数栈的生命周期,编译器就不会进行堆分配
    2. 事实上,只要代码逻辑允许,编译器总是倾向于把变量分配在栈上,因为比配在堆上更高效。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
TEXT main.New(SB) /mnt/hgfs/workspace/helium/main.go
func New() int {
  0x455240        4883ec20            SUBQ $0x20, SP        
  0x455244        48896c2418          MOVQ BP, 0x18(SP)    
  0x455249        488d6c2418          LEAQ 0x18(SP), BP    
  0x45524e        48c7042400000000    MOVQ $0x0, 0(SP)   # 返回地址 int
    p := new(int)
    # new 函数直接是在栈上分配的
  0x455256        48c744240800000000  MOVQ $0x0, 0x8(SP) # 0x8(SP) 用作int类型存储 0
  0x45525f        488d4c2408          LEAQ 0x8(SP), CX   # CX=0x8(SP); 也就是 *int
  0x455264        48894c2410          MOVQ CX, 0x10(SP)  # 0x10(SP); *int
    return *p
  0x455269        8401                TESTB AL, 0(CX)        
  0x45526b        488b442408          MOVQ 0x8(SP), AX    
  0x455270        48890424            MOVQ AX, 0(SP)        
  0x455274        488b6c2418          MOVQ 0x18(SP), BP    
  0x455279        4883c420            ADDQ $0x20, SP        
  0x45527d        c3                  RET
  1. 这也就是本节所谓的不逃逸分析,或者说未逃逸分析,这种说法并不严谨,主要是为了突出编译器倾向于让变量不逃逸

不逃逸判断

包级别指针(1)

  1. 本节主要探索编译器进行逃逸分析时追踪的范围,以及在什么情况下就认为变量逃逸了或者确定变量没有逃逸。
  2. 前面研究变量逃逸所有的方法,主要通过让函数返回局部变量的地址,使局部变量的生命周期超过对应函数栈帧的生命周期。
  3. 按照这个规则来猜想,如果把局部变量的地址赋值给包级别的指针变量,应该也会造成变量逃逸。
  4. 准备一个示例,代码如下:
1
2
3
4
5
6
7
var pt *int

//go:noinline
func setNew() {
    var a int
    pt = &a
}
  1. 反编译 setNew() 函数,代码如下:
 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
TEXT main.setNew(SB) /mnt/hgfs/workspace/helium/main.go
func setNew() {
  0x455220        493b6610              CMPQ 0x10(R14), SP          # 栈增长判断
  0x455224        765d                  JBE 0x455283        
  0x455226        4883ec20              SUBQ $0x20, SP        
  0x45522a        48896c2418            MOVQ BP, 0x18(SP)    
  0x45522f        488d6c2418            LEAQ 0x18(SP), BP    
    var a int
  0x455234        488d05e54a0000        LEAQ 0x4ae5(IP), AX         # type.int 元类型
  0x45523b        0f1f440000            NOPL 0(AX)(AX*1)        
  # func newobject(typ *_type) unsafe.Pointer
  # 这里直接调用了 newobject 堆分配了 int
  0x455240        e8fb5cfbff            CALL runtime.newobject(SB)  # *int
  0x455245        4889442410            MOVQ AX, 0x10(SP)        
  0x45524a        48c70000000000        MOVQ $0x0, 0(AX)            # a = 0    
    pt = &a
  0x455251        488b4c2410            MOVQ 0x10(SP), CX           # CX=0x10(SP)=*int
  # 判断是否开启写屏障,因为可能在GC阶段,变量的赋值需要写屏障
  0x455256        833d0320090000        CMPL $0x0, runtime.writeBarrier(SB)    
  0x45525d        7403                  JE 0x455262                
  0x45525f        90                    NOPL                    
  0x455260        eb09                  JMP 0x45526b                
  0x455262        48890db7320600        MOVQ CX, main.pt(SB)        # 给全局变量 pt 赋值    
  0x455269        eb0e                  JMP 0x455279                
  0x45526b        488d3dae320600        LEAQ main.pt(SB), DI            
  # 写屏障开启的时候调用写屏障相关函数,记录指针的变更,已帮助GC
  0x455272        e8a9d0ffff            CALL runtime.gcWriteBarrierCX(SB)
  0x455277        eb00                  JMP 0x455279                
}
  0x455279        488b6c2418            MOVQ 0x18(SP), BP    
  0x45527e        4883c420              ADDQ $0x20, SP        
  0x455282        c3                    RET            
func setNew() {
  0x455283        e8d8ccffff            CALL runtime.morestack_noctxt.abi0(SB)    
  0x455288        eb96                  JMP main.setNew(SB)
  1. 通过 runtime.newobject() 函数调用就能确定,变量a逃逸到了堆上,验证了上述猜想。

包级别指针(2)

  1. 进一步还可以验证逃逸分析的依赖传递性,准备示例代码如下:
1
2
3
4
5
6
7
8
9
var pp **int

//go:noinline
func dep() {
    var a int
    var p *int
    p = &a
    pp = &p
}
  1. 反编译 dep() 函数:
    1. 可以发现,变量 p 和 a 都逃逸了。
    2. p 的地址被赋值给包级别的指针变量 pp,而 a 的地址又被赋值给了 p,因为 p 逃逸造成 a 也逃逸了。
 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
53
54
55
56
57
58
TEXT main.dep(SB) /mnt/hgfs/workspace/helium/main.go
func dep() {
  0x455220        493b6610              CMPQ 0x10(R14), SP          # 栈增长判断
  0x455224        0f86b0000000          JBE 0x4552da        
  0x45522a        4883ec28              SUBQ $0x28, SP        
  0x45522e        48896c2420            MOVQ BP, 0x20(SP)    
  0x455233        488d6c2420            LEAQ 0x20(SP), BP    
    var a int
  0x455238        488d05e14a0000        LEAQ 0x4ae1(IP), AX         # type.int 元类型;
  0x45523f        90            NOPL        
  # func newobject(typ *_type) unsafe.Pointer
  0x455240        e8fb5cfbff            CALL runtime.newobject(SB)  # 堆分配了 a 变量; AX *int
  0x455245        4889442418            MOVQ AX, 0x18(SP)           # 0x18(SP); 是a的内存空间,指向堆分配数据
  0x45524a        48c70000000000        MOVQ $0x0, 0(AX)            # 初始化 0
    var p *int
  0x455251        488d0508320000        LEAQ 0x3208(IP), AX         # type *int 元类型
  # func newobject(typ *_type) unsafe.Pointer
  0x455258        e8e35cfbff            CALL runtime.newobject(SB)  # **int
  0x45525d        4889442410            MOVQ AX, 0x10(SP)            
  0x455262        833df71f090000        CMPL $0x0, runtime.writeBarrier(SB)    
  0x455269        7402                  JE 0x45526d                
  0x45526b        eb09                  JMP 0x455276                
  0x45526d        48c70000000000        MOVQ $0x0, 0(AX)            
  0x455274        eb11                  JMP 0x455287                
  0x455276        4889c7                MOVQ AX, DI                
  0x455279        31c0                  XORL AX, AX                
  0x45527b        0f1f440000            NOPL 0(AX)(AX*1)            
  0x455280        e89bcfffff            CALL runtime.gcWriteBarrier(SB)        
  0x455285        eb00                  JMP 0x455287                
    p = &a
  0x455287        488b7c2410            MOVQ 0x10(SP), DI            
  0x45528c        488b442418            MOVQ 0x18(SP), AX            
  0x455291        833dc81f090000        CMPL $0x0, runtime.writeBarrier(SB)    
  0x455298        7402                  JE 0x45529c                
  0x45529a        eb06                  JMP 0x4552a2                
  0x45529c        488907                MOVQ AX, 0(DI)                
  0x45529f        90                    NOPL                    
  0x4552a0        eb07                  JMP 0x4552a9                
  0x4552a2        e879cfffff            CALL runtime.gcWriteBarrier(SB)        
  0x4552a7        eb00                  JMP 0x4552a9                
    pp = &p
  0x4552a9        488b442410            MOVQ 0x10(SP), AX            
  0x4552ae        833dab1f090000        CMPL $0x0, runtime.writeBarrier(SB)    
  0x4552b5        7402                  JE 0x4552b9                
  0x4552b7        eb09                  JMP 0x4552c2                
  0x4552b9        48890560320600        MOVQ AX, main.pp(SB)            
  0x4552c0        eb0e                  JMP 0x4552d0                
  0x4552c2        488d3d57320600        LEAQ main.pp(SB), DI            
  0x4552c9        e852cfffff            CALL runtime.gcWriteBarrier(SB)        
  0x4552ce        eb00                  JMP 0x4552d0                
}
  0x4552d0        488b6c2420            MOVQ 0x20(SP), BP    
  0x4552d5        4883c428              ADDQ $0x28, SP        
  0x4552d9        c3                    RET            
func dep() {
  0x4552da        e881ccffff            CALL runtime.morestack_noctxt.abi0(SB)    
  0x4552df        90                    NOPL                    
  0x4552e0        e93bffffff            JMP main.dep(SB)
  1. 假如某个函数有一个参数和一个返回值,类型都是整型指针,函数只是简单地把参数作为返回值。

在函数间传递

  1. 就像下面的 gom.RetArg() 函数,代码如下:
1
2
3
4
5
6
package gom

//go:noinline
func RetArg(p *int) *int {
    return p
}
  1. 在另一个包中 arg() 函数调用了 inner.RetArg() 函数将局部变量 a 的地址作为参数,并返回了一个 int 类型的返回值。
  2. 代码如下:
1
2
3
4
5
6
7
package main

//go:noinline
func arg() int {
    var a int
    return *gom.RetArg(&a)
}
  1. 在 arg() 函数中并没有把变量 a 的地址作为返回值,也不存在到某个包级别指针变量的依赖链路,所以变量a是否会逃逸的关键就在于inner.RetArg()函数。
  2. inner. RetArg()函数只是把传过去的指针又传了回来,而且作为被调用者来讲,它的生命周期是完全包含在arg()函数的生命周期以内的,所以不应该造成变量 a 逃逸。
  3. 事实到底如何呢? 还要通过反编译验证,节选部分关键汇编代码如下:
    1. 变量a确实是在栈上分配的,也就说明编译器参考了inner.RetArg()函数的具体实现,基于代码逻辑判定变量 a 没有逃逸。
    2. 虽然代码中通过**noinline阻止了内联优化,但是没能阻止编译器参考函数实现**。
    3. 假如通过某种方式能够阻止编译器参考函数实现,又会有什么样的结果呢?
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
TEXT main.arg(SB) /mnt/hgfs/workspace/helium/main.go
func arg() int {
  0x455240        493b6610              CMPQ 0x10(R14), SP    # 栈增长判断
  0x455244        7643                  JBE 0x455289        
  0x455246        4883ec28              SUBQ $0x28, SP        
  0x45524a        48896c2420            MOVQ BP, 0x20(SP)    
  0x45524f        488d6c2420            LEAQ 0x20(SP), BP    
  0x455254        48c744240800000000    MOVQ $0x0, 0x8(SP)    # return int
    var a int
  0x45525d        48c744241000000000    MOVQ $0x0, 0x10(SP)    
    return *gom.RetArg(&a)
    # 可以看出这里调用gom.RetArg函数时参数直接是在栈上分配的,并没有堆分配
  0x455266        488d442410            LEAQ 0x10(SP), AX            
  0x45526b        e870ffffff            CALL example.com/helium/gom.RetArg(SB)    
  0x455270        4889442418            MOVQ AX, 0x18(SP)            
  0x455275        8400                  TESTB AL, 0(AX)                
  0x455277        488b00                MOVQ 0(AX), AX                
  0x45527a        4889442408            MOVQ AX, 0x8(SP)            
  0x45527f        488b6c2420            MOVQ 0x20(SP), BP            
  0x455284        4883c428              ADDQ $0x28, SP                
  0x455288        c3                    RET                    
func arg() int {
  0x455289        e8d2ccffff            CALL runtime.morestack_noctxt.abi0(SB)    
  0x45528e        ebb0                  JMP main.arg(SB)

linkname 机制

  1. 可以使用 linkname 机制,连同修改后的 arg() 函数的代码如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "example.com/helium/gom"
    _ "unsafe" // 使用了 go:linkname 必须要引入 unsafe 包
)

func main() {
    arg()
}

//go:linkname retArg example.com/helium/gom.RetArg
func retArg(p *int) *int

//go:noinline
func arg() int {
    var a int
    var b int
    return *gom.RetArg(&a) + retArg(&b)
}
  1. 再次反编译 arg() 函数,节选变量 a 和 b 分配相关的汇编代码如下:
    1. 变量 a 依旧是栈分配,变量 b 已经逃逸了。
    2. 在上述代码中的 retArg() 函数只是个函数声明没有给出具体实现,通过 linkname 机制让链接器在链接阶段链接到 inner.RetArg() 函数。
    3. retArg() 函数只有声明没有实现,而且编译器不会跟踪 linkname,所以无法根据代码逻辑判定变量 b 到底有没有逃逸。
 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
TEXT main.arg(SB) /mnt/hgfs/workspace/helium/main.go
func arg() int {
  0x455240        493b6610              CMPQ 0x10(R14), SP    # 栈增长判断
  0x455244        7677                  JBE 0x4552bd        
  0x455246        4883ec40              SUBQ $0x40, SP        
  0x45524a        48896c2438            MOVQ BP, 0x38(SP)    
  0x45524f        488d6c2438            LEAQ 0x38(SP), BP    
  0x455254        48c744241000000000    MOVQ $0x0, 0x10(SP)    # return int
    var a int
  0x45525d        48c744241800000000    MOVQ $0x0, 0x18(SP)    # a,0
    var b int
  0x455266        488d05b34a0000        LEAQ 0x4ab3(IP), AX    # type.int
  0x45526d        e8ce5cfbff            CALL runtime.newobject(SB)    # *int
  0x455272        4889442430            MOVQ AX, 0x30(SP) # 变量b,堆分配了
  0x455277        48c70000000000        MOVQ $0x0, 0(AX)        
    return *gom.RetArg(&a) + *retArg(&b)
  0x45527e        488d442418            LEAQ 0x18(SP), AX            
  0x455283        e858ffffff            CALL example.com/helium/gom.RetArg(SB)    
  0x455288        4889442428            MOVQ AX, 0x28(SP)            
  0x45528d        488b442430            MOVQ 0x30(SP), AX            
  0x455292        e849ffffff            CALL example.com/helium/gom.RetArg(SB)    
  0x455297        4889442420            MOVQ AX, 0x20(SP)            
  0x45529c        488b4c2428            MOVQ 0x28(SP), CX            
  0x4552a1        8401                  TESTB AL, 0(CX)                
  0x4552a3        8400                  TESTB AL, 0(AX)                
  0x4552a5        488b09                MOVQ 0(CX), CX                
  0x4552a8        480308                ADDQ 0(AX), CX                
  0x4552ab        48894c2410            MOVQ CX, 0x10(SP)            
  0x4552b0        4889c8                MOVQ CX, AX                
  0x4552b3        488b6c2438            MOVQ 0x38(SP), BP            
  0x4552b8        4883c440              ADDQ $0x40, SP                
  0x4552bc        c3                    RET                    
func arg() int {
  0x4552bd        0f1f00                NOPL 0(AX)                
  0x4552c0        e89bccffff            CALL runtime.morestack_noctxt.abi0(SB)    
  0x4552c5        e976ffffff            JMP main.arg(SB)
  1. 把逻辑上没有逃逸的变量分配到堆上不会造成错误,只是效率低一些。
  2. 但是把逻辑上逃逸了的变量分配到栈上就会造成悬挂指针等问题。
  3. 因此编译器只有在能够确定变量没有逃逸的情况下,才会将其分配到栈上,在能够确定变量已经逃逸或无法确定到底有没有逃逸的情况下,都要按照已经逃逸来处理。
  4. 这也就解释了为什么在上述代码中的变量 b 逻辑上没逃逸却被分配在了堆上。

函数示例

示例一

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

func main() {
    sum(1, 2)
}

// sum 计算a, b的平方和
func sum(a, b int) int {
    a2 := a * a
    b2 := b * b
    c := a2 + b2

    return c
}
  1. main.main汇编:
 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
TEXT main.main(SB) /mnt/hgfs/g/hello1/hello.go
    # 判断栈是否溢出 0x10(R14)指向g.stackguard0位置
    hello.go:3    0x45b4a0    493b6610      cmp rsp, qword ptr [r14+0x10]          cmp 0x10(R14), rsp    
    # 跳转到0x45b4cf处设置栈信息,在重新调main.main函数
    hello.go:3    0x45b4a4    7629          jbe 0x45b4cf                           jbe 0x45b4cf        
    # 为当前栈预分配参数空间,第一个8B存储runtime.main的rbp,第二个8B存储main.sun的参数b为1,第三个8B存储main.sun的参数a为2
    hello.go:3    0x45b4a6    4883ec18      sub rsp, 0x18                          sub 0x18, rsp        
    # 把runtime.main的栈rbp存入0x10(rsp)处
    hello.go:3    0x45b4aa    48896c2410    mov qword ptr [rsp+0x10], rbp          mov rbp, 0x10(rsp)        
    # 从新设置当前main.main的栈rbp值为0x10(rsp)处
    hello.go:3    0x45b4af    488d6c2410    lea rbp, ptr [rsp+0x10]                lea 0x10(rsp), rbp        
    # 为main.sum准备第一个参数,放入寄存器AX中
    hello.go:4    0x45b4b4    b801000000    mov eax, 0x1                           mov 0x1, AX            
    # 为main.sum准备第二个参数,放入寄存器BX中
    hello.go:4    0x45b4b9    bb02000000    mov ebx, 0x2                           mov 0x2, BX                
    hello.go:4    0x45b4be    6690          data16 nop
    hello.go:4    0x45b4c0    e81b000000    call $main.sum                         call $main.sum
    # 把runtime.main的栈rbp值从0x10(rsp)处放入rbp寄存器
    hello.go:5    0x45b4c5    488b6c2410    mov rbp, qword ptr [rsp+0x10]          mov 0x10(rsp), rbp        
    # rsp寄存器恢复调用main.main之前情况
    hello.go:5    0x45b4ca    4883c418      add rsp, 0x18                          add 0x18, rsp        
    # 返回,rsp-8弹出返回地址给rip返回的runtime.main继续执行
    hello.go:5    0x45b4ce    c3            ret                                    ret
    hello.go:3    0x45b4cf    e88ccdffff    call $runtime.morestack_noctxt         call $runtime.morestack_noctxt
    .:0           0x45b4d4    ebca          jmp $main.main                         jmp $main.main
  1. main.sum汇编:
 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
TEXT main.sum(SB) /mnt/hgfs/g/hello1/hello.go
    # 为main.sum栈预分配0x28空间
    hello.go:8    0x45b4e0    4883ec28          sub rsp, 0x28                        sub 0x28, rsp
    # 把main.main的rbp存储到0x20(rsp)位置
    hello.go:8    0x45b4e4    48896c2420        mov qword ptr [rsp+0x20], rbp        mov rbp, 0x20(rsp)
    # 从新设置main.sum的rbp值
    hello.go:8    0x45b4e9    488d6c2420        lea rbp, ptr [rsp+0x20]              lea 0x20(rsp), rbp
    # 把传递的第一个参数入栈,注意看这里参参数栈分配在main.main的栈上
    hello.go:8    0x45b4ee    4889442430        mov qword ptr [rsp+0x30], rax        mov AX, 0x30(rsp)    # a = 1
    # 把传递的第二个参数入栈,注意看这里参参数栈分配在main.main的栈上
    hello.go:8    0x45b4f3    48895c2438        mov qword ptr [rsp+0x38], rbx        mov BX, 0x38(rsp)    # b = 2
    # 初始化rsp的第一个8字节吗,返回值空间初始化
    hello.go:8    0x45b4f8    48c7042400000000  mov qword ptr [rsp], 0x0             mov 0x0, (rsp)       # return
    # 为乘法运算做准备,把变量a的值放入寄存器
    hello.go:9    0x45b500    488b4c2430        mov rcx, qword ptr [rsp+0x30]        mov 0x30(rsp), CX        
    # 为乘法运算做准备,把变量a的值放入寄存器
    hello.go:9    0x45b505    488b542430        mov rdx, qword ptr [rsp+0x30]        mov 0x30(rsp), DX        
    # 乘法运算,结果存储在DX中
    hello.go:9    0x45b50a    480fafd1          imul rdx, rcx                        imul CX, DX          # a*a        
    # 0X18(rsp)的地址为变量a2
    hello.go:9    0x45b50e    4889542418        mov qword ptr [rsp+0x18], rdx        mov DX, 0X18(rsp)    # a2=a*a    
    # 为乘法运算做准备,把变量b的值放入寄存器
    hello.go:10    0x45b513    488b4c2438       mov rcx, qword ptr [rsp+0x38]        mov 0x38(rsp), CX    
    # 为乘法运算做准备,把变量b的值放入寄存器
    hello.go:10    0x45b518    488b542438       mov rdx, qword ptr [rsp+0x38]        mov 0x38(rsp), DX        
    # 乘法运算,结果存储在DX中
    hello.go:10    0x45b51d    480fafd1         imul rdx, rcx                        imul CX, DX  # b*b        
    # 0X10(rsp)的地址为变量b2
    hello.go:10    0x45b521    4889542410       mov qword ptr [rsp+0x10], rdx        mov DX, 0x10(rsp)    # b2 = b*b    
    # 把a2值放入CX寄存器
    hello.go:11    0x45b526    488b4c2418       mov rcx, qword ptr [rsp+0x18]        mov 0x18(rsp), CX        
    # 计算 a2 + b2
    hello.go:11    0x45b52b    488d0411         lea rax, ptr [rcx+rdx*1]             lea (CX+DX*1), AX    # a2+b2    
    # a2 + b2 值存入0x8(rsp)是变量的值
    hello.go:11    0x45b52f    4889442408       mov qword ptr [rsp+0x8], rax         mov AX, 0x8(rsp)     # c = a2+b2    
    # a2 + b2 值存入(rsp) 该值是返回值空间
    hello.go:13    0x45b534    48890424         mov qword ptr [rsp], rax             mov AX, (rsp)        # return        
    # 弹出main.main的rbp
    hello.go:13    0x45b538    488b6c2420       mov rbp, qword ptr [rsp+0x20]        mov 0x20(rsp), rbp        
    # 使rsp指向main.main的栈rsp位置
    .:0            0x45b53d    4883c428         add rsp, 0x28                        add 0x28, rsp            
    # 弹出main.main的下一条指令地址
    .:0            0x45b541    c3               ret                                  ret
  1. 栈分布情况:
// 我们看一下代码在14行时也就是main.sum函数返回前的栈布局情况
//
//                            栈底
//                ----------------------------    高地址
//               | b
//                ----------------------------
//               | a                                            runtime.main函数栈帧
//                ----------------------------
//               |   返回到runtime.main的地址        +0x48
// -----------> ---------------------------- <-------------------------------------
//               |   调用函数runtime.main的rbp       +0x40                
//                ----------------------------                          
//               |   main.sum的第二个参数 0x2        +0x38                
//                ----------------------------                main.main函数栈帧
//               |   main.sum的第一个参数 0x1        +0x30                
//                 ----------------------------                         
//               |   返回到main.main的地址           +0x28                
// -----------> ---------------------------- <-------------------------------------
//               |   调用函数main.main的rbp          +0x20               
//                 ----------------------------                        
//               |   变量a2   0x1                   +0x18               
//                 ----------------------------                        
//               |   变量b2   0x4                   +0x10        main.sum函数栈帧
//                 ----------------------------                        
//               |   变量c   0x5                    +0x8                
//                 ----------------------------                        
//               |   main.sum的返回值地址 0x5        +0x0                
// -----------> ---------------------------- <-------------------------------------

编译指令

  1. 编译指令:go build -gcflags="-N -l" slice1.go。
  2. 查看具体方法的汇编指令:go tool objdump -S -s ‘^main.main$’ slice1。