探索golang程序启动过程


go version == 1.9.3

GOARCH=“amd64”

GOOS=“darwin”

本文探索下go程序是如何启动起来的。

测试工程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// $GOPATH/test/main.go
package main

import (
	"fmt"
)

func main() {
	fmt.Println("hello world")
}

使用gdb跟踪查看程序启动流程

通过设置断点,可以定位到每个方法的源码文件路径

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$ go build main.go
$ gdb main

GNU gdb (GDB) 7.12.1
Copyright (C) 2017 Free Software Foundation, Inc.
(gdb) info files
Symbols from "/Users/cbsheng/goproject/src/test/main".
Local exec file:
	`/Users/cbsheng/goproject/src/test/main', file type mach-o-x86-64.
	Entry point: 0x1051540  # 入口点
	0x0000000001001000 - 0x000000000109352b is .text
	0x0000000001093540 - 0x00000000010d6e52 is __TEXT.__rodata
	0x00000000010d6e52 - 0x00000000010d6e52 is __TEXT.__symbol_stub1
	...

(gdb) b *0x1051540
Breakpoint 1 at 0x1051540: file /usr/local/go/src/runtime/rt0_darwin_amd64.s, line 8.
(gdb) b runtime.rt0_go
Breakpoint 2 at 0x104dd30: file /usr/local/go/src/runtime/asm_amd64.s, line 12.

针对不同的平台都有各自的特定的汇编文件,我这里通过rt0_darwin_amd64.s定位到runtime.rt0_go方法。先来看下asm_amd64.s的源码。文件很大,省略部分代码,留下初始化过程的重要步骤。

注意:汇编的runtime·schedinit(SB)写法需要转换为runtime.schedinit(即中间的点不同)。官方有解释

In Go object files and binaries, the full name of a symbol is the package path followed by a period and the symbol name: fmt.Printf or math/rand.Int. Because the assembler’s parser treats period and slash as punctuation, those strings cannot be used directly as identifier names. Instead, the assembler allows the middle dot character U+00B7 and the division slash U+2215 in identifiers and rewrites them to plain period and slash. Within an assembler source file, the symbols above are written as fmt·Printf and math∕rand·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
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// src/runtime/asm_amd64.s
// 省略头文件

TEXT runtime·rt0_go(SB),NOSPLIT,$0
    // 省略命令行参数处理的代码

	// 全局的g0实例地址放到DI
	MOVQ	$runtime·g0(SB), DI
	LEAQ	(-64*1024+104)(SP), BX
	// 初始化全局g0实例的stackguard0\stackguard1\stack这三个字段
	MOVQ	BX, g_stackguard0(DI)
	MOVQ	BX, g_stackguard1(DI)
	MOVQ	BX, (g_stack+stack_lo)(DI)
	MOVQ	SP, (g_stack+stack_hi)(DI)

	// 省略一系列与CPU相关的特性标志位检查的代码

nocpuinfo:
    // 到这里与CPU相关的检查已设置完毕
	// if there is an _cgo_init, call it.
	MOVQ	_cgo_init(SB), AX
	// 没开启cgo的话就跳转到needtls标签继续执行
	TESTQ	AX, AX
	JZ	needtls
	// g0 already in DI
	MOVQ	DI, CX	// Win64 uses CX for first parameter
	MOVQ	$setg_gcc<>(SB), SI
	CALL	AX

	// _cgo_init 执行完,重新更新下stackguard0/stackguard1
	MOVQ	$runtime·g0(SB), CX
	MOVQ	(g_stack+stack_lo)(CX), AX
	ADDQ	$const__StackGuard, AX
	MOVQ	AX, g_stackguard0(CX)
	MOVQ	AX, g_stackguard1(CX)

needtls:
	// 省略部分用于预编译的代码,因为本文的测试工程环境不会执行这些代码

	// 取全局m0实例的tls字段地址放在DI,并进行设置
	LEAQ	runtime·m0+m_tls(SB), DI
	CALL	runtime·settls(SB)

	// store through it, to make sure it works
	// 经过上面的settls后,需要验证get_tls和g()作用是否符合预期。失败的话,就引发abort
	get_tls(BX)
	MOVQ	$0x123, g(BX)
	MOVQ	runtime·m0+m_tls(SB), AX
	CMPQ	AX, $0x123
	JEQ 2(PC)
	MOVL	AX, 0	// abort

ok:
	// 验证成功后,把全局g0实例放进tls,并且将g0和m0互相引用
	get_tls(BX)
	LEAQ	runtime·g0(SB), CX
	MOVQ	CX, g(BX)
	LEAQ	runtime·m0(SB), AX

	// save m->g0 = g0
	MOVQ	CX, m_g0(AX)
	// save m0 to g0->m
	MOVQ	AX, g_m(CX)

	CLD				// convention is D is always left cleared
	CALL	runtime·check(SB)

	MOVL	16(SP), AX		// copy argc
	MOVL	AX, 0(SP)
	MOVQ	24(SP), AX		// copy argv
	MOVQ	AX, 8(SP)
	// 执行文件的绝对路径初始化
	CALL	runtime·args(SB)
	// cpu个数和内存页大小初始化
	CALL	runtime·osinit(SB)
	// 命令行参数、环境变量、gc、栈空间、内存管理、所有P实例、HASH算法等初始化
	CALL	runtime·schedinit(SB)

	// create a new goroutine to start program
	// runtime.main函数地址放进AX
	MOVQ	$runtime·mainPC(SB), AX		// entry
	// 推进栈
	PUSHQ	AX
	PUSHQ	$0			// arg size
	// 新建一个goroutine,该goroutine绑定runtime.main,放在P的本地队列,等待调度
	CALL	runtime·newproc(SB)
	POPQ	AX
	POPQ	AX

	// start this M
	// 启动M,开始调度goroutine
	CALL	runtime·mstart(SB)

	MOVL	$0xf1, 0xf1  // crash
	RET

DATA	runtime·mainPC+0(SB)/8,$runtime·main(SB)

按顺序总结下runtime.rt0_go里几件重要的事:

  1. 检查运行平台的CPU,设置好程序运行需要相关标志。
  2. TLS的初始化。
  3. runtime.args、runtime.osinit、runtime.schedinit 三个方法做好程序运行需要的各种变量与调度器。
  4. runtime.newproc创建新的goroutine用于绑定用户写的main方法。
  5. runtime.mstart开始goroutine的调度。

下面接着针对上面几个runtime函数,粗略探索下干了什么事情。

runtime.args

只做了一件事,就是把二进制文件的绝对路径找出来,并存在os.executablePath里。

按照本文的测试工程:os.executablePath=$GOPATH/test/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
// (gdb) b runtime.args
// Breakpoint 3 at 0x1035040: file /usr/local/go/src/runtime/runtime1.go, line 60.
func args(c int32, v **byte) {
    argc = c
    argv = v
    sysargs(c, v)
}

//go:linkname executablePath os.executablePath
var executablePath string

func sysargs(argc int32, argv **byte) {
    // skip over argv, envv and the first string will be the path
    n := argc + 1
    for argv_index(argv, n) != nil {
        n++
    }
    executablePath = gostringnocopy(argv_index(argv, n+1))

    // strip "executable_path=" prefix if available, it's added after OS X 10.11.
    const prefix = "executable_path="
    if len(executablePath) > len(prefix) && executablePath[:len(prefix)] == prefix {
        executablePath = executablePath[len(prefix):]
    }
}

runtime.osinit

获取CPU核数与内存页大小。按照本文的测试工程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// (gdb) b runtime.osinit
// Breakpoint 4 at 0x10247e0: file /usr/local/go/src/runtime/os_darwin.go, line 48.

// BSD interface for threading.
func osinit() {
    // bsdthread_register delayed until end of goenvs so that we
    // can look at the environment first.

    ncpu = getncpu()

    physPageSize = getPageSize()
}

runtime.schedinit

这个直接在代码中注释吧

 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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// (gdb) b runtime.schedinit
// Breakpoint 5 at 0x1029b60: file /usr/local/go/src/runtime/proc.go, line 458.

// The bootstrap sequence is:
//
//	call osinit
//	call schedinit
//	make & queue new G
//	call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
	// raceinit must be the first call to race detector.
	// In particular, it must be done before mallocinit below calls racemapshadow.
	// 从TLS中获取g实例
    _g_ := getg()

    if raceenabled {
        _g_.racectx, raceprocctx0 = raceinit()
    }

    // 设置全局线程数上限
    sched.maxmcount = 10000

    // 初始化一系列函数所在的PC计数器,用于traceback
    tracebackinit()
    // 貌似是验证链接器符号的正确性
    moduledataverify()
    // 栈的初始化
    stackinit()
    // 内存分配器初始化
    mallocinit()
    mcommoninit(_g_.m)
    // 初始化AES HASH算法
    alginit()       // maps must not be used before this call
    modulesinit()   // provides activeModules
    typelinksinit() // uses maps, activeModules
    itabsinit()     // uses activeModules

    msigsave(_g_.m)
    initSigmask = _g_.m.sigmask

    // 获取命令行参数
    // 例如: ./$GOPATH/test/main test1 test2
    // 执行goargs得到runtime.argslice = []string len: 3, cap: 3, ["main","test1","test2"]
    goargs()
    // 获取所有的环境变量
    goenvs()
    parsedebugvars()
    // gc初始化
    gcinit()

    sched.lastpoll = uint64(nanotime())

    // P个数检查
    procs := ncpu
    if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n
    }
    if procs > _MaxGomaxprocs {
        procs = _MaxGomaxprocs
    }
	// 所有P的初始化
    if procresize(procs) != nil {
        throw("unknown runnable goroutine during bootstrap")
    }

    if buildVersion == "" {
        // Condition should never trigger. This code just serves
        // to ensure runtime·buildVersion is kept in the resulting binary.
        buildVersion = "unknown"
    }
}

runtime.newproc

参考探索goroutine的创建

runtime.mstart

mstart方法主要的执行路径是:

mstart -> mstart1 -> schedule -> execute

  1. mstart做一些栈相关的检查,然后就调用mstart1。
  2. mstart1先做一些初始化与M相关的工作,例如是信号栈和信号处理函数的初始化。最后调用schedule。
  3. schedule逻辑是这四个方法里最复杂的。简单来说,就是要找出一个可运行的G,不管是从P本地的G队列、全局调度器的G队列、GC worker、因IO阻塞的G、甚至从别的P里偷。然后传给execute运行。
  4. execute对传进来的G设置好相关的状态后,就加载G自身记录着的PC、SP等寄存器信息,恢复现场继续执行。

参考

Go语言内幕(5):运行时启动过程

Go语言学习笔记