runtime的作用

runtime被编译为程序的一部分,一起执行。code+runtime=>二进制。

runtime

  • 内存管理
  • 垃圾回收 (gc)
  • 协程调度. (超强并发能力)
  • 有一定的屏蔽系统调用的能力
  • 一些go 关键字其实是runtime 的函数
    • go => newproc
    • new => newobject
    • make => makeslice,makechain,makemap …
    • <- -> chansend1,chanrecv1

go程序是如何编译的

go build -n 查看编译过程,不实际编译

Go 编译过程: 词法分析->句法分析->语义分析-> 中间码生成->代码优化->机器码生成(.a文件)-> 链接

  • 词法分析

    将源代码翻译成 token,token 是代码中的最小语义结构

  • 句法分析

    token序列经过处理,变成语法树

  • 语义分析:类型检查、类型推断、函数内敛优化、查看类型是否匹配、逃逸分析

  • 中间码生成(ssa)

    为了处理不同平台的差异,先生成中间代码(ssa) (汇编)

    查看从代码到中间码的整个过程

    1   GOSSAFUNC=main go build test.go
    
    1      ➜  bdsp git:(mao_main)  GOSSAFUNC=main go build test.go
    2     # runtime
    3       dumped SSA to /Users/maozhongyu/code/bdsp/ssa.html
    4     # command-line-arguments
    5     dumped SSA to ./ssa.html
    
  • 机器码生成

    先生成plan9汇编代码(平台相关的汇编) ,最后编译为机器码 , 输出机器码为.a文件

    查看plan9汇编代码

    1  go build -gcflags -S main.go
    
  • 链接

    将各个包进行链接,包括runtime,生成二进制可执行文件

go程序是怎么运行的?

本文源码是针对go1.18.5进行分析

使用go build -x 观察编译连接过程

1go build -x hello.go 

可执行文件

Linux 的可执行文件 ELF(Executable and Linkable Format) 为例,

ELF 由几部分构成:

  • ELF header
  • Section header
  • Sections

操作系统执行可执行文件的步骤

解析 ELF header,加载文件到内存,从entry point 开始执行代码

readelf 配合dlv 找到程序入口

通过readelf -H中的entry找到程序⼊⼝

 1[ubuntu@us_inner ~ 14:31:07]$readelf -h ./hello
 2ELF Header:
 3  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
 4  Class:                             ELF64
 5  Data:                              2's complement, little endian
 6  Version:                           1 (current)
 7  OS/ABI:                            UNIX - System V
 8  ABI Version:                       0
 9  Type:                              EXEC (Executable file)
10  Machine:                           Advanced Micro Devices X86-64
11  Version:                           0x1
12  Entry point address:               0x45c020  (入口地址)
13  Start of program headers:          64 (bytes into file)
14  Start of section headers:          456 (bytes into file)
15  Flags:                             0x0
16  Size of this header:               64 (bytes)
17  Size of program headers:           56 (bytes)
18  Number of program headers:         7
19  Size of section headers:           64 (bytes)
20  Number of section headers:         23
21  Section header string table index: 3

在dlv调试器中b *entry_addr找到代码位置

1sudo /usr/local/go/bin/go  get github.com/go-delve/delve/cmd/dlv
2sudo /usr/local/go/bin/go install github.com/go-delve/delve/cmd/dlv
3
4
5[ubuntu@us_inner ~ 14:39:41]$/home/ubuntu/go/bin/dlv exec ./hello
6Type 'help' for list of commands.
7(dlv) b *0x45c020
8Breakpoint 1 set at 0x45c020 for _rt0_amd64_linux() /usr/local/go/src/runtime/rt0_linux_amd64.s:8

代码分析运行流程详解

上面是linux的,这里是用mac进行分析。

mac,amd64 cpu系统,通过追代码分析,大致流程。

rt0_darwin_amd64.s

1TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8
2	JMP	_rt0_amd64(SB) // 第一步 go程序的入口 : runtime/rt0_平台

asm_amd64.s

 1TEXT _rt0_amd64(SB),NOSPLIT,$-8
 2	MOVQ	0(SP), DI	// argc    //第二步  读取命令行参数,复制参数数量argc和参数值argv到栈上 
 3	LEAQ	8(SP), SI	// argv 
 4	JMP	runtime·rt0_go(SB)    
 5	
 6
 7TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0   
 8	// copy arguments forward on an even stack
 9	MOVQ	DI, AX		// argc
10	MOVQ	SI, BX		// argv
11	SUBQ	$(5*8), SP		// 3args 2auto
12	ANDQ	$~15, SP
13	MOVQ	AX, 24(SP)
14	MOVQ	BX, 32(SP)
15
16	// create istack out of the given (operating system) stack.
17	// _cgo_init may update stackguard.
18	MOVQ	$runtime·g0(SB), DI    // 第三步  初始化g0的执行栈,g0是为了调度协程而产生的协程
19	LEAQ	(-64*1024+104)(SP), BX
20	MOVQ	BX, g_stackguard0(DI)
21	MOVQ	BX, g_stackguard1(DI)
22	MOVQ	BX, (g_stack+stack_lo)(DI)
23	MOVQ	SP, (g_stack+stack_hi)(DI)
24
25    .  //此处代码省略
26    .
27    .
28    
29     // runtime1.go check() 方法
30    CALL	runtime·check(SB) //314 行 左右。 第四步  运行时检测:检测各种类型的长度、检测指针操作、检测结构体字段的偏移量、检测atomic的原子操作、检测cas操作 等
31    
32	MOVL	24(SP), AX		// copy argc
33	MOVL	AX, 0(SP)
34	MOVQ	32(SP), AX		// copy argv
35	MOVQ	AX, 8(SP)
36	CALL	runtime·args(SB)     // 第5步  处理命令行参数runtime.args()
37	CALL	runtime·osinit(SB)   // 系统字长,cpu是多少核第。一些int 是多少字节的处理等。
38	CALL	runtime·schedinit(SB)  //第6步 调度器初始化 runtime.schedinit
39	
40		// create a new goroutine to start program
41	MOVQ	$runtime·mainPC(SB), AX		// entry  // runtime中的main方法地址
42	PUSHQ	AX
43	CALL	runtime·newproc(SB)         //  第7步 创建主协程 。 创建一个新的协程 ,用来执行runtime.main,等待调度
44	POPQ	AX
45
46	// start this M
47	CALL	runtime·mstart(SB)       //第8步 初始化一个m ,用来调度主协程。 
48
49	CALL	runtime·abort(SB)	// mstart should never return
50	RET
51	
52	bad_cpu: // show that the program requires a certain microarchitecture level.
53	MOVQ	$2, 0(SP)
54	MOVQ	$bad_cpu_msg<>(SB), AX
55	MOVQ	AX, 8(SP)
56	MOVQ	$84, 16(SP)
57	CALL	runtime·write(SB)
58	MOVQ	$1, 0(SP)
59	CALL	runtime·exit(SB)
60	CALL	runtime·abort(SB)
61	RET
62
63	// Prevent dead-code elimination of debugCallV2, which is
64	// intended to be called by debuggers.
65	MOVQ	$runtime·debugCallV2<ABIInternal>(SB), AX
66	RET
67
68// mainPC is a function value for runtime.main, to be passed to newproc.
69// The reference to runtime.main is made via ABIInternal, since the
70// actual function (not the ABI0 wrapper) is needed by newproc.
71DATA	runtime·mainPC+0(SB)/8,$runtime·main<ABIInternal>(SB)  // 主协程执行主函数 runtime 包的main 方法,  runtime/proc.go 里的main方法。
72GLOBL	runtime·mainPC(SB),RODATA,$8
73    

runtime1.go

第四步运行时检测: 检测各种类型的长度、检测指针操作、检测结构体字段的偏移量、检测atomic的原子操作、检测cas操作 等

 1func check() {
 2	var (
 3		a     int8
 4		b     uint8
 5		c     int16
 6		d     uint16
 7		e     int32
 8		f     uint32
 9		g     int64
10		h     uint64
11		i, i1 float32
12		j, j1 float64
13		k     unsafe.Pointer
14		l     *uint16
15		m     [4]byte
16	)
17	type x1t struct {
18		x uint8
19	}
20	type y1t struct {
21		x1 x1t
22		y  uint8
23	}
24	var x1 x1t
25	var y1 y1t
26
27	if unsafe.Sizeof(a) != 1 {
28		throw("bad a")
29	}
30	if unsafe.Sizeof(b) != 1 {
31		throw("bad b")
32	}
33    //此处省略
34    //....
35		
36	if unsafe.Offsetof(y1.y) != 1 {
37		throw("bad offsetof y1.y")
38	}
39	if unsafe.Sizeof(y1) != 2 {
40		throw("bad unsafe.Sizeof y1")
41	}
42
43	if timediv(12345*1000000000+54321, 1000000000, &e) != 12345 || e != 54321 {
44		throw("bad timediv")
45	}
46
47	var z uint32
48	z = 1
49	if !atomic.Cas(&z, 1, 2) {
50		throw("cas1")
51	}
52	if z != 2 {
53		throw("cas2")
54	}
55
56//此处省略
57//....
58	
59	if z != 0xfffffffe {
60		throw("cas6")
61	}
62
63	m = [4]byte{1, 1, 1, 1}
64	atomic.Or8(&m[1], 0xf0)
65	if m[0] != 1 || m[1] != 0xf1 || m[2] != 1 || m[3] != 1 {
66		throw("atomicor8")
67	}
68
69	m = [4]byte{0xff, 0xff, 0xff, 0xff}
70	atomic.And8(&m[1], 0x1)
71	if m[0] != 0xff || m[1] != 0x1 || m[2] != 0xff || m[3] != 0xff {
72		throw("atomicand8")
73	}
74
75	*(*uint64)(unsafe.Pointer(&j)) = ^uint64(0)
76	if j == j {
77		throw("float64nan")
78	}
79	if !(j != j) {
80		throw("float64nan1")
81	}
82
83	*(*uint64)(unsafe.Pointer(&j1)) = ^uint64(1)
84	if j == j1 {
85		throw("float64nan2")
86	}
87    // ....省略
88
89	testAtomic64()
90
91	if _FixedStack != round2(_FixedStack) {
92		throw("FixedStack is not power-of-2")
93	}
94
95	if !checkASM() {
96		throw("assembly checks failed")
97	}
98}

runtime/proc.go

  1
  2
  3// start forcegc helper goroutine
  4// 第9步 执行runtime包的init方法 
  5func init() {
  6	go forcegchelper()
  7}
  8
  9func forcegchelper() {
 10	forcegc.g = getg()
 11	// 第10步 启动gc垃圾回收器
 12	lockInit(&forcegc.lock, lockRankForcegc)
 13	for {
 14		lock(&forcegc.lock)
 15		if forcegc.idle != 0 {
 16			throw("forcegc: phase error")
 17		}
 18		atomic.Store(&forcegc.idle, 1)
 19		goparkunlock(&forcegc.lock, waitReasonForceGCIdle, traceEvGoBlock, 1)
 20		// this goroutine is explicitly resumed by sysmon
 21		if debug.gctrace > 0 {
 22			println("GC forced")
 23		}
 24		// Time-triggered, fully concurrent.
 25		gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()})
 26	}
 27}
 28
 29
 30//第11步  执行runtime包的main方法
 31// The main goroutine.
 32func main() {
 33	g := getg()
 34
 35	// Racectx of m0->g0 is used only as the parent of the main goroutine.
 36	// It must not be used for anything else.
 37	g.m.g0.racectx = 0
 38
 39	// Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
 40	// Using decimal instead of binary GB and MB because
 41	// they look nicer in the stack overflow failure message.
 42	if goarch.PtrSize == 8 {
 43		maxstacksize = 1000000000
 44	} else {
 45		maxstacksize = 250000000
 46	}
 47
 48	// An upper limit for max stack size. Used to avoid random crashes
 49	// after calling SetMaxStack and trying to allocate a stack that is too big,
 50	// since stackalloc works with 32-bit sizes.
 51	maxstackceiling = 2 * maxstacksize
 52
 53	// Allow newproc to start new Ms.
 54	mainStarted = true
 55
 56	if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
 57		systemstack(func() {
 58			newm(sysmon, nil, -1)
 59		})
 60	}
 61
 62	// Lock the main goroutine onto this, the main OS thread,
 63	// during initialization. Most programs won't care, but a few
 64	// do require certain calls to be made by the main thread.
 65	// Those can arrange for main.main to run in the main thread
 66	// by calling runtime.LockOSThread during initialization
 67	// to preserve the lock.
 68	lockOSThread()
 69
 70	if g.m != &m0 {
 71		throw("runtime.main not on m0")
 72	}
 73
 74	// Record when the world started.
 75	// Must be before doInit for tracing init.
 76	runtimeInitTime = nanotime()
 77	if runtimeInitTime == 0 {
 78		throw("nanotime returning zero")
 79	}
 80
 81	if debug.inittrace != 0 {
 82		inittrace.id = getg().goid
 83		inittrace.active = true
 84	}
 85
 86	doInit(&runtime_inittask) // Must be before defer.
 87
 88	// Defer unlock so that runtime.Goexit during init does the unlock too.
 89	needUnlock := true
 90	defer func() {
 91		if needUnlock {
 92			unlockOSThread()
 93		}
 94	}()
 95
 96	gcenable()
 97
 98	main_init_done = make(chan bool)
 99	if iscgo {
100		if _cgo_thread_start == nil {
101			throw("_cgo_thread_start missing")
102		}
103		if GOOS != "windows" {
104			if _cgo_setenv == nil {
105				throw("_cgo_setenv missing")
106			}
107			if _cgo_unsetenv == nil {
108				throw("_cgo_unsetenv missing")
109			}
110		}
111		if _cgo_notify_runtime_init_done == nil {
112			throw("_cgo_notify_runtime_init_done missing")
113		}
114		// Start the template thread in case we enter Go from
115		// a C-created thread and need to create a new thread.
116		startTemplateThread()
117		cgocall(_cgo_notify_runtime_init_done, nil)
118	}
119
120	// 第12步 执行用户包依赖的init方法
121	doInit(&main_inittask)
122
123	// Disable init tracing after main init done to avoid overhead
124	// of collecting statistics in malloc and newproc
125	inittrace.active = false
126
127	close(main_init_done)
128
129	needUnlock = false
130	unlockOSThread()
131
132	if isarchive || islibrary {
133		// A program compiled with -buildmode=c-archive or c-shared
134		// has a main, but it is not executed.
135		return
136	}
137	
138	// 第13步 执行用户主函数 main.main(), 这个才是我们平常main包中的main方法
139	fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
140	fn()
141	if raceenabled {
142		racefini()
143	}
144
145	// Make racy client program work: if panicking on
146	// another goroutine at the same time as main returns,
147	// let the other goroutine finish printing the panic trace.
148	// Once it does, it will exit. See issues 3934 and 20018.
149	if atomic.Load(&runningPanicDefers) != 0 {
150		// Running deferred functions should not take long.
151		for c := 0; c < 1000; c++ {
152			if atomic.Load(&runningPanicDefers) == 0 {
153				break
154			}
155			Gosched()
156		}
157	}
158	if atomic.Load(&panicking) != 0 {
159		gopark(nil, nil, waitReasonPanicWait, traceEvGoStop, 1)
160	}
161
162	exit(0)
163	for {
164		var x *int32
165		*x = 0
166	}
167}

流程总结

  1. go程序的入口 : runtime/rt0_平台.s,如 rt0_darwin_amd64.s

  2. 读取命令行参数,复制参数数量argc和参数值argv到栈上

  3. 初始化g0的执行栈,g0是为了调度协程而产生的协程

    上面代码就是初始化go

    G0 是每个go程序的 第一个协程 (万物之母) (汇编代码?)

  4. 运行时检测:检测各种类型的长度、检测指针操作、检测结构体字段的偏移量、检测atomic的原子操作、检测cas操作 等

  5. 处理命令行参数runtime.args()

  6. 调度器初始化 runtime.schedinit

  • 全局栈空间内存分配
  • 加载命令参数到os.Args
  • 堆内存空间的初始化
  • 加载操作系统环境变量
  • 初始化当前系统线程
  • 垃圾回收器参数初始化
  • 算法初始化(map、hash)
  • 设置proess数量(gpm中的p)
  1. 创建主协程

    创建一个新的协程,执行runtime.main

    放入调度器等待调度

  2. 初始化一个m ,用来调度主协程

  3. 执行runtime包的init方法

    主协程要去调runtime包的main方法,那么先要执行它的init方法 和用户包init 顺序是一致的

  4. 启动gc垃圾回收器

  5. 主协程执行主函数

    runtime包的main方法

  6. 执行用户包依赖的init方法

  7. 执行用户主函数 main.main()

go 启动时经历了检查,各种初始化,初始化协成调度的过程 ,main.main() 也是在协程中运行的

Go 编译器 SSA 中间代码探究