Golang笔记–07–Linux环境下的Go程序启动流程
文章目录
1. 前言
1.1 可执行文件及调试文件
elf, dwarf and Golang. 本来英文版的标题应该是这样的,翻译成中文后就变成妖精、矮人与Golang(气场突然提升),最后还是觉得不用这个标题了。
Linux环境下ELF全称是Executable and Linkable Format,DWARF全称为Debugging With Attributed Record Formats。
无论使用什么语言编写程序,在Linux环境下编译后都会输出一份ELF文件。ELF是可执行文件、object code、shared library和core dumps的标准文件格式。默认构建指令编译出的Go程序包含调试信息,以dwarf格式压缩后存储,gdb、delve等通过读取调试信息提供debug功能。下述输出示例都是在amd64架构,Linux环境下操作的。
关于ELF信息不再赘述,更多的具体资料如下:
- Go标准库:debug/elf
- ELF文件结构简介:Executable and Linkable Format
- ELF文件ProgramHeader、SectionHeader具体结构:The 101 of ELF files on Linux: Understanding and Analysis
- ELF文件内存布局:Understanding the Memory Layout of Linux Executables
- ELF文件如何加载运行:How programs get run: ELF binaries
1.2 编译器
从代码到可执行文件一般经历过词法分析、语法分析、编译、汇编、链接等过程,而在Go语言中,我们可见的命令有
- go tool compile:处理go文件,执行词法分析、语法分析、汇编、编译,输出obj文件
- go tool asm:处理汇编文件(.s文件),输出obj文件
- go tool pack:打包package下的所有obj文件,输出.a文件
- go tool link:链接不同package的.a文件,输出可执行文件
- go tool objdump:反汇编obj文件
- go tool nm:输出obj文件、.a文件或可执行文件中定义的符号
Go汇编代码是一种Plan 9风格汇编代码,我们甚至可以使用go tool objdump去反汇编一个C程序,可以得到Plan 9风格的汇编代码,反之也可以使用objdump反汇编Go程序,得到x86风格的汇编代码。
关于编译器的资料,以下两份文档都值得一读
2. 程序启动入口
下面的命令可以分别构建出无调试信息的最小可知程序、默认包含压缩调试信息的程序、包含无压缩调试信息的程序
1// 移除调试信息构建可执行程序
2go build -ldflags "-s -w" -o hello cmd/hello/main.go
3// 包含压缩dwarf格式的可执行程序(默认)
4go build -o hello cmd/hello/main.go
5// 关闭dwarf压缩构建可执行程序
6go build -ldflags "-compressdwarf=false" -o hello cmd/hello/main.go
执行readelf -e hello
读取包含调试信息的可执行文件,得到完整的ELF头部输出
1ELF Header:
2 Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
3 Class: ELF64
4 Data: 2's complement, little endian
5 Version: 1 (current)
6 OS/ABI: UNIX - System V
7 ABI Version: 0
8 Type: EXEC (Executable file)
9 Machine: Advanced Micro Devices X86-64
10 Version: 0x1
11 Entry point address: 0x4605e0
12 Start of program headers: 64 (bytes into file)
13 Start of section headers: 456 (bytes into file)
14 Flags: 0x0
15 Size of this header: 64 (bytes)
16 Size of program headers: 56 (bytes)
17 Number of program headers: 7
18 Size of section headers: 64 (bytes)
19 Number of section headers: 25
20 Section header string table index: 3
21
22Section Headers:
23 [Nr] Name Type Address Offset
24 Size EntSize Flags Link Info Align
25 [ 0] NULL 0000000000000000 00000000
26 0000000000000000 0000000000000000 0 0 0
27 [ 1] .text PROGBITS 0000000000401000 00001000
28 00000000009ef97a 0000000000000000 AX 0 0 16
29 [ 2] .rodata PROGBITS 0000000000df1000 009f1000
30 000000000046b19a 0000000000000000 A 0 0 32
31 [ 3] .shstrtab STRTAB 0000000000000000 00e5c1a0
32 00000000000001a1 0000000000000000 0 0 1
33 [ 4] .typelink PROGBITS 000000000125c360 00e5c360
34 0000000000008174 0000000000000000 A 0 0 32
35 [ 5] .itablink PROGBITS 00000000012644d8 00e644d8
36 0000000000002868 0000000000000000 A 0 0 8
37 [ 6] .gosymtab PROGBITS 0000000001266d40 00e66d40
38 0000000000000000 0000000000000000 A 0 0 1
39 [ 7] .gopclntab PROGBITS 0000000001266d40 00e66d40
40 0000000000652074 0000000000000000 A 0 0 32
41 [ 8] .go.buildinfo PROGBITS 00000000018b9000 014b9000
42 0000000000000020 0000000000000000 WA 0 0 16
43 [ 9] .noptrdata PROGBITS 00000000018b9020 014b9020
44 000000000004e920 0000000000000000 WA 0 0 32
45 [10] .data PROGBITS 0000000001907940 01507940
46 0000000000015cd0 0000000000000000 WA 0 0 32
47 [11] .bss NOBITS 000000000191d620 0151d620
48 000000000002a170 0000000000000000 WA 0 0 32
49 [12] .noptrbss NOBITS 00000000019477a0 015477a0
50 0000000000003af8 0000000000000000 WA 0 0 32
51 [13] .zdebug_abbrev PROGBITS 000000000194c000 0151e000
52 0000000000000119 0000000000000000 0 0 8
53 [14] .zdebug_line PROGBITS 000000000194c119 0151e119
54 000000000011074c 0000000000000000 0 0 8
55 [15] .zdebug_frame PROGBITS 0000000001a5c865 0162e865
56 000000000004c5c3 0000000000000000 0 0 8
57 [16] .zdebug_pubnames PROGBITS 0000000001aa8e28 0167ae28
58 00000000000089ed 0000000000000000 0 0 8
59 [17] .zdebug_pubtypes PROGBITS 0000000001ab1815 01683815
60 00000000000228ed 0000000000000000 0 0 8
61 [18] .debug_gdb_script PROGBITS 0000000001ad4102 016a6102
62 0000000000000024 0000000000000000 0 0 1
63 [19] .zdebug_info PROGBITS 0000000001ad4126 016a6126
64 00000000001fa247 0000000000000000 0 0 8
65 [20] .zdebug_loc PROGBITS 0000000001cce36d 018a036d
66 0000000000159fa8 0000000000000000 0 0 8
67 [21] .zdebug_ranges PROGBITS 0000000001e28315 019fa315
68 0000000000071a60 0000000000000000 0 0 8
69 [22] .note.go.buildid NOTE 0000000000400f9c 00000f9c
70 0000000000000064 0000000000000000 A 0 0 4
71 [23] .symtab SYMTAB 0000000000000000 01a6c000
72 00000000000b5440 0000000000000018 24 542 8
73 [24] .strtab STRTAB 0000000000000000 01b21440
74 0000000000171f69 0000000000000000 0 0 1
75Key to Flags:
76 W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
77 L (link order), O (extra OS processing required), G (group), T (TLS),
78 C (compressed), x (unknown), o (OS specific), E (exclude),
79 l (large), p (processor specific)
80
81Program Headers:
82 Type Offset VirtAddr PhysAddr
83 FileSiz MemSiz Flags Align
84 PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
85 0x0000000000000188 0x0000000000000188 R 1000
86 NOTE 0x0000000000000f9c 0x0000000000400f9c 0x0000000000400f9c
87 0x0000000000000064 0x0000000000000064 R 4
88 LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
89 0x00000000009f097a 0x00000000009f097a R E 1000
90 LOAD 0x00000000009f1000 0x0000000000df1000 0x0000000000df1000
91 0x0000000000ac7db4 0x0000000000ac7db4 R 1000
92 LOAD 0x00000000014b9000 0x00000000018b9000 0x00000000018b9000
93 0x0000000000064620 0x0000000000092298 RW 1000
94 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
95 0x0000000000000000 0x0000000000000000 RW 8
96 LOOS+5041580 0x0000000000000000 0x0000000000000000 0x0000000000000000
97 0x0000000000000000 0x0000000000000000 8
98
99 Section to Segment mapping:
100 Segment Sections...
101 00
102 01 .note.go.buildid
103 02 .text .note.go.buildid
104 03 .rodata .typelink .itablink .gosymtab .gopclntab
105 04 .go.buildinfo .noptrdata .data .bss .noptrbss
106 05
107 06
执行readelf -e hello
读取不包含调试信息的可执行文件,得到完整的ELF头部输出
1ELF Header:
2 Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
3 Class: ELF64
4 Data: 2's complement, little endian
5 Version: 1 (current)
6 OS/ABI: UNIX - System V
7 ABI Version: 0
8 Type: EXEC (Executable file)
9 Machine: Advanced Micro Devices X86-64
10 Version: 0x1
11 Entry point address: 0x4605e0
12 Start of program headers: 64 (bytes into file)
13 Start of section headers: 456 (bytes into file)
14 Flags: 0x0
15 Size of this header: 64 (bytes)
16 Size of program headers: 56 (bytes)
17 Number of program headers: 7
18 Size of section headers: 64 (bytes)
19 Number of section headers: 14
20 Section header string table index: 3
21
22Section Headers:
23 [Nr] Name Type Address Offset
24 Size EntSize Flags Link Info Align
25 [ 0] NULL 0000000000000000 00000000
26 0000000000000000 0000000000000000 0 0 0
27 [ 1] .text PROGBITS 0000000000401000 00001000
28 00000000009ef97a 0000000000000000 AX 0 0 16
29 [ 2] .rodata PROGBITS 0000000000df1000 009f1000
30 000000000046b19a 0000000000000000 A 0 0 32
31 [ 3] .shstrtab STRTAB 0000000000000000 00e5c1a0
32 000000000000008a 0000000000000000 0 0 1
33 [ 4] .typelink PROGBITS 000000000125c240 00e5c240
34 0000000000008174 0000000000000000 A 0 0 32
35 [ 5] .itablink PROGBITS 00000000012643b8 00e643b8
36 0000000000002868 0000000000000000 A 0 0 8
37 [ 6] .gosymtab PROGBITS 0000000001266c20 00e66c20
38 0000000000000000 0000000000000000 A 0 0 1
39 [ 7] .gopclntab PROGBITS 0000000001266c20 00e66c20
40 0000000000652074 0000000000000000 A 0 0 32
41 [ 8] .go.buildinfo PROGBITS 00000000018b9000 014b9000
42 0000000000000020 0000000000000000 WA 0 0 16
43 [ 9] .noptrdata PROGBITS 00000000018b9020 014b9020
44 000000000004e920 0000000000000000 WA 0 0 32
45 [10] .data PROGBITS 0000000001907940 01507940
46 0000000000015cd0 0000000000000000 WA 0 0 32
47 [11] .bss NOBITS 000000000191d620 0151d620
48 000000000002a170 0000000000000000 WA 0 0 32
49 [12] .noptrbss NOBITS 00000000019477a0 015477a0
50 0000000000003af8 0000000000000000 WA 0 0 32
51 [13] .note.go.buildid NOTE 0000000000400f9c 00000f9c
52 0000000000000064 0000000000000000 A 0 0 4
53Key to Flags:
54 W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
55 L (link order), O (extra OS processing required), G (group), T (TLS),
56 C (compressed), x (unknown), o (OS specific), E (exclude),
57 l (large), p (processor specific)
58
59Program Headers:
60 Type Offset VirtAddr PhysAddr
61 FileSiz MemSiz Flags Align
62 PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
63 0x0000000000000188 0x0000000000000188 R 1000
64 NOTE 0x0000000000000f9c 0x0000000000400f9c 0x0000000000400f9c
65 0x0000000000000064 0x0000000000000064 R 4
66 LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
67 0x00000000009f097a 0x00000000009f097a R E 1000
68 LOAD 0x00000000009f1000 0x0000000000df1000 0x0000000000df1000
69 0x0000000000ac7c94 0x0000000000ac7c94 R 1000
70 LOAD 0x00000000014b9000 0x00000000018b9000 0x00000000018b9000
71 0x0000000000064620 0x0000000000092298 RW 1000
72 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
73 0x0000000000000000 0x0000000000000000 RW 8
74 LOOS+5041580 0x0000000000000000 0x0000000000000000 0x0000000000000000
75 0x0000000000000000 0x0000000000000000 8
76
77 Section to Segment mapping:
78 Segment Sections...
79 00
80 01 .note.go.buildid
81 02 .text .note.go.buildid
82 03 .rodata .typelink .itablink .gosymtab .gopclntab
83 04 .go.buildinfo .noptrdata .data .bss .noptrbss
84 05
85 06
可以看出上述两者之间最大的区别就是section数量,包含调试信息的可执行文件有更多的debug信息,可以通过dwarfdump工具导出dwarf文件
通过网上的资料我们已知:
- Program Headers告诉操作系统如何创建一个运行时的内存镜像,操作系统使用mmap将这些分段映射到虚拟地址空间
- Section Headers定义了链接和重定位的数据,每个Section都包含起始地址及长度,根据类型划分为
- text:可执行代码
- data:已初始化数据,可读写,例如已初始化的全局变量
- rodata:已初始化数据,只读,例如全局常量
- .bss:未初始化数据,可读写,例如未初始化的全局变量
- 其他
以上述包含调试信息的ELF文件为例,接下来寻找程序启动的入口
从ELF文件头部(Entry point address)可以得到程序入口虚拟地址为:0x4605e0,正好落在编号为1的section:text(虚拟地址空间为0x401000~0xdf097a),包含可执行代码,属于只读section。
然后使用readelf获取导出符号表,寻找0x4605e0,执行readelf -s hello|grep 4605e0
19049: 00000000004605e0 5 FUNC GLOBAL DEFAULT 1 _rt0_amd64_linux
可见Linux下的Go程序启动入口代码为**_rt0_amd64_linux**,接下来可以回到源码了
3. runtime启动代码
"I could be bounded in a nutshell, and count myself a king of infinite space, were it not that I have bad dreams." - Hamlet
在看过runtime代码后,突然想到哈姆雷特的这句话,没看过启动代码的话,永远不会知道自己的代码都封闭在一个个goroutine中
我们可以在Go标准库中的runtime/rt0_linux_amd64.s文件找到上一小节中的入口代码定义
1TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
2 JMP _rt0_amd64(SB)
执行跳转到runtime/asm_amd64.s中的启动代码
1// _rt0_amd64 is common startup code for most amd64 systems when using
2// internal linking. This is the entry point for the program from the
3// kernel for an ordinary -buildmode=exe program. The stack holds the
4// number of arguments and the C-style argv.
5TEXT _rt0_amd64(SB),NOSPLIT,$-8
6 MOVQ 0(SP), DI // argc
7 LEAQ 8(SP), SI // argv
8 JMP runtime·rt0_go(SB)
上述代码最终携带启动程序信息及命令行参数,跳转到了同一文件下的另一段汇编代码入口,这段代码十分长,但概括下来就是
- 获取命令行参数
- 获取CPU和操作系统信息
- 初始化runtime
- 启动main函数,runtime的main函数间接启动用户main函数
- 等待main函数退出,处理debug信息
1TEXT runtime·rt0_go(SB),NOSPLIT,$0
2 // copy arguments forward on an even stack
3 MOVQ DI, AX // argc
4 MOVQ SI, BX // argv
5 SUBQ $(4*8+7), SP // 2args 2auto
6 ANDQ $~15, SP
7 MOVQ AX, 16(SP)
8 MOVQ BX, 24(SP)
9 // create istack out of the given (operating system) stack.
10 // _cgo_init may update stackguard.
11 MOVQ $runtime·g0(SB), DI
12 LEAQ (-64*1024+104)(SP), BX
13 MOVQ BX, g_stackguard0(DI)
14 MOVQ BX, g_stackguard1(DI)
15 MOVQ BX, (g_stack+stack_lo)(DI)
16 MOVQ SP, (g_stack+stack_hi)(DI)
17 // find out information about the processor we're on
18 MOVL $0, AX
19 CPUID
20 MOVL AX, SI
21 CMPL AX, $0
22 JE nocpuinfo
23 // Figure out how to serialize RDTSC.
24 // On Intel processors LFENCE is enough. AMD requires MFENCE.
25 // Don't know about the rest, so let's do MFENCE.
26 CMPL BX, $0x756E6547 // "Genu"
27 JNE notintel
28 CMPL DX, $0x49656E69 // "ineI"
29 JNE notintel
30 CMPL CX, $0x6C65746E // "ntel"
31 JNE notintel
32 MOVB $1, runtime·isIntel(SB)
33 MOVB $1, runtime·lfenceBeforeRdtsc(SB)
34notintel:
35 // Load EAX=1 cpuid flags
36 MOVL $1, AX
37 CPUID
38 MOVL AX, runtime·processorVersionInfo(SB)
39nocpuinfo:
40 // if there is an _cgo_init, call it.
41 MOVQ _cgo_init(SB), AX
42 TESTQ AX, AX
43 JZ needtls
44 // arg 1: g0, already in DI
45 MOVQ $setg_gcc<>(SB), SI // arg 2: setg_gcc
46#ifdef GOOS_android
47 MOVQ $runtime·tls_g(SB), DX // arg 3: &tls_g
48 // arg 4: TLS base, stored in slot 0 (Android's TLS_SLOT_SELF).
49 // Compensate for tls_g (+16).
50 MOVQ -16(TLS), CX
51#else
52 MOVQ $0, DX // arg 3, 4: not used when using platform's TLS
53 MOVQ $0, CX
54#endif
55#ifdef GOOS_windows
56 // Adjust for the Win64 calling convention.
57 MOVQ CX, R9 // arg 4
58 MOVQ DX, R8 // arg 3
59 MOVQ SI, DX // arg 2
60 MOVQ DI, CX // arg 1
61#endif
62 CALL AX
63 // update stackguard after _cgo_init
64 MOVQ $runtime·g0(SB), CX
65 MOVQ (g_stack+stack_lo)(CX), AX
66 ADDQ $const__StackGuard, AX
67 MOVQ AX, g_stackguard0(CX)
68 MOVQ AX, g_stackguard1(CX)
69#ifndef GOOS_windows
70 JMP ok
71#endif
72needtls:
73#ifdef GOOS_plan9
74 // skip TLS setup on Plan 9
75 JMP ok
76#endif
77#ifdef GOOS_solaris
78 // skip TLS setup on Solaris
79 JMP ok
80#endif
81#ifdef GOOS_illumos
82 // skip TLS setup on illumos
83 JMP ok
84#endif
85#ifdef GOOS_darwin
86 // skip TLS setup on Darwin
87 JMP ok
88#endif
89 LEAQ runtime·m0+m_tls(SB), DI
90 CALL runtime·settls(SB)
91 // store through it, to make sure it works
92 get_tls(BX)
93 MOVQ $0x123, g(BX)
94 MOVQ runtime·m0+m_tls(SB), AX
95 CMPQ AX, $0x123
96 JEQ 2(PC)
97 CALL runtime·abort(SB)
98ok:
99 // set the per-goroutine and per-mach "registers"
100 get_tls(BX)
101 LEAQ runtime·g0(SB), CX
102 MOVQ CX, g(BX)
103 LEAQ runtime·m0(SB), AX
104 // save m->g0 = g0
105 MOVQ CX, m_g0(AX)
106 // save m0 to g0->m
107 MOVQ AX, g_m(CX)
108 CLD // convention is D is always left cleared
109 CALL runtime·check(SB)
110 MOVL 16(SP), AX // copy argc
111 MOVL AX, 0(SP)
112 MOVQ 24(SP), AX // copy argv
113 MOVQ AX, 8(SP)
114 CALL runtime·args(SB)
115 CALL runtime·osinit(SB)
116 CALL runtime·schedinit(SB)
117 // create a new goroutine to start program
118 MOVQ $runtime·mainPC(SB), AX // entry
119 PUSHQ AX
120 PUSHQ $0 // arg size
121 CALL runtime·newproc(SB)
122 POPQ AX
123 POPQ AX
124 // start this M
125 CALL runtime·mstart(SB)
126 CALL runtime·abort(SB) // mstart should never return
127 RET
128 // Prevent dead-code elimination of debugCallV1, which is
129 // intended to be called by debuggers.
130 MOVQ $runtime·debugCallV1(SB), AX
131 RET
132DATA runtime·mainPC+0(SB)/8,$runtime·main(SB)
133GLOBL runtime·mainPC(SB),RODATA,$8
3.1 参数拷贝
将栈上的参数拷贝到寄存器中
- MOVQ指将第二个操作数中的数据拷贝4个字的长度到第一个操作数中
- ANDQ、SUBQ同样操作4个字的数据,第二个操作数是数据源,第一个操作数是目标
1// copy arguments forward on an even stack
2 MOVQ DI, AX // argc
3 MOVQ SI, BX // argv
4 SUBQ $(4*8+7), SP // 2args 2auto
5 ANDQ $~15, SP
6 MOVQ AX, 16(SP)
7 MOVQ BX, 24(SP)
3.2 同步主线程系统栈
- 运行时定义了全局变量g0及m0,m0是启动用户main函数的线程
- 由于runtime未初始化,需要手动绑定m0及g0,并让g0使用进程创建后的主线程栈空间
1 // create istack out of the given (operating system) stack.
2 // _cgo_init may update stackguard.
3 MOVQ $runtime·g0(SB), DI
4 LEAQ (-64*1024+104)(SP), BX
5 MOVQ BX, g_stackguard0(DI)
6 MOVQ BX, g_stackguard1(DI)
7 MOVQ BX, (g_stack+stack_lo)(DI)
8 MOVQ SP, (g_stack+stack_hi)(DI)
3.3 获取CPU信息
- 执行一个CPUID指令,尝试获取CPU信息
- 用特定参数判断CPU为通用类型、英特尔类型、非英特尔类型
1 // find out information about the processor we're on
2 MOVL $0, AX
3 CPUID
4 MOVL AX, SI
5 CMPL AX, $0
6 JE nocpuinfo
7
8 // Figure out how to serialize RDTSC.
9 // On Intel processors LFENCE is enough. AMD requires MFENCE.
10 // Don't know about the rest, so let's do MFENCE.
11 CMPL BX, $0x756E6547 // "Genu"
12 JNE notintel
13 CMPL DX, $0x49656E69 // "ineI"
14 JNE notintel
15 CMPL CX, $0x6C65746E // "ntel"
16 JNE notintel
17 MOVB $1, runtime·isIntel(SB)
18 MOVB $1, runtime·lfenceBeforeRdtsc(SB)
19notintel:
20
21 // Load EAX=1 cpuid flags
22 MOVL $1, AX
23 CPUID
24 MOVL AX, runtime·processorVersionInfo(SB)
3.4 配置TLS(Thread Local Storage)
TLS表示限定于一个线程内的静态或全局变量,具体概念可以查看维基百科:Thread Local Storage
另外也涉及runtime/go_tls.h文件,内容如下
1#ifdef GOARCH_arm
2#define LR R14
3#endif
4
5#ifdef GOARCH_amd64
6#define get_tls(r) MOVQ TLS, r
7#define g(r) 0(r)(TLS*1)
8#endif
9
10#ifdef GOARCH_amd64p32
11#define get_tls(r) MOVL TLS, r
12#define g(r) 0(r)(TLS*1)
13#endif
14
15#ifdef GOARCH_386
16#define get_tls(r) MOVL TLS, r
17#define g(r) 0(r)(TLS*1)
18#endif
可以看到代码中定义了TLS相关的宏,主要是x86架构的实现,其他架构应该是直接使用m结构体中的tls字段
下面的代码中,主要执行了以下操作
- 尝试初始化cgo
- 根据操作系统配置TLS
- 更新g0系统栈
- 调用TLS以测试是否可用
1nocpuinfo:
2 // if there is an _cgo_init, call it.
3 MOVQ _cgo_init(SB), AX
4 TESTQ AX, AX
5 JZ needtls
6 // arg 1: g0, already in DI
7 MOVQ $setg_gcc<>(SB), SI // arg 2: setg_gcc
8#ifdef GOOS_android
9 MOVQ $runtime·tls_g(SB), DX // arg 3: &tls_g
10 // arg 4: TLS base, stored in slot 0 (Android's TLS_SLOT_SELF).
11 // Compensate for tls_g (+16).
12 MOVQ -16(TLS), CX
13#else
14 MOVQ $0, DX // arg 3, 4: not used when using platform's TLS
15 MOVQ $0, CX
16#endif
17#ifdef GOOS_windows
18 // Adjust for the Win64 calling convention.
19 MOVQ CX, R9 // arg 4
20 MOVQ DX, R8 // arg 3
21 MOVQ SI, DX // arg 2
22 MOVQ DI, CX // arg 1
23#endif
24 CALL AX
25
26 // update stackguard after _cgo_init
27 MOVQ $runtime·g0(SB), CX
28 MOVQ (g_stack+stack_lo)(CX), AX
29 ADDQ $const__StackGuard, AX
30 MOVQ AX, g_stackguard0(CX)
31 MOVQ AX, g_stackguard1(CX)
32
33#ifndef GOOS_windows
34 JMP ok
35#endif
36needtls:
37#ifdef GOOS_plan9
38 // skip TLS setup on Plan 9
39 JMP ok
40#endif
41#ifdef GOOS_solaris
42 // skip TLS setup on Solaris
43 JMP ok
44#endif
45#ifdef GOOS_illumos
46 // skip TLS setup on illumos
47 JMP ok
48#endif
49#ifdef GOOS_darwin
50 // skip TLS setup on Darwin
51 JMP ok
52#endif
53
54 LEAQ runtime·m0+m_tls(SB), DI
55 CALL runtime·settls(SB)
56
57 // store through it, to make sure it works
58 get_tls(BX)
59 MOVQ $0x123, g(BX)
60 MOVQ runtime·m0+m_tls(SB), AX
61 CMPQ AX, $0x123
62 JEQ 2(PC)
63 CALL runtime·abort(SB)
3.5 绑定g0与m0、检查基础数据类型
- 已完成TLS配置,配置goroutine和主线程的虚拟寄存器
- 绑定g0与m0
- 执行基础数据类型检查,check函数位于runtime/runtime1.go文件中
1ok:
2 // set the per-goroutine and per-mach "registers"
3 get_tls(BX)
4 LEAQ runtime·g0(SB), CX
5 MOVQ CX, g(BX)
6 LEAQ runtime·m0(SB), AX
7
8 // save m->g0 = g0
9 MOVQ CX, m_g0(AX)
10 // save m0 to g0->m
11 MOVQ AX, g_m(CX)
12
13 CLD // convention is D is always left cleared
14 CALL runtime·check(SB)
3.6 初始化调度器并启动main函数
虽然主线程早已启动,我们也执行大量的汇编指令和函数调用,但直到调用m0的mstart启动调度前,一切都还是静止的状态,下面的代码中主要执行了以下操作
- 参数拷贝
- runtime.args:执行参数初始化,位于runtime/runtime1.go文件中
- runtime.osinit:执行操作系统相关初始化,位于runtime下os开头的go文件中,主要是获取CPU数量与HugePage大小
- runtime.schedinit:执行调度器初始化,位于runtime/proc.go文件中
- 创建一个启动main函数的goroutine,加入m0的g队列中,它将会是接下来第一个启动的goroutine
- runtime.mstart:启动m0,开始执行调度
1 MOVL 16(SP), AX // copy argc
2 MOVL AX, 0(SP)
3 MOVQ 24(SP), AX // copy argv
4 MOVQ AX, 8(SP)
5 CALL runtime·args(SB)
6 CALL runtime·osinit(SB)
7 CALL runtime·schedinit(SB)
8
9 // create a new goroutine to start program
10 MOVQ $runtime·mainPC(SB), AX // entry
11 PUSHQ AX
12 PUSHQ $0 // arg size
13 CALL runtime·newproc(SB)
14 POPQ AX
15 POPQ AX
16
17 // start this M
18 CALL runtime·mstart(SB)
19
20 CALL runtime·abort(SB) // mstart should never return
21 RET
22
23 // Prevent dead-code elimination of debugCallV1, which is
24 // intended to be called by debuggers.
25 MOVQ $runtime·debugCallV1(SB), AX
26 RET
上述代码中的mainPC其实就是runtime的main函数
1DATA runtime·mainPC+0(SB)/8,$runtime·main(SB)
2GLOBL runtime·mainPC(SB),RODATA,$8
runtime.main位于runtime/proc.go文件中
3.7 调度器初始化
这里需要拆解一下调度器初始化函数,一如既往,其中的每个函数调用都能展开成一节,但是概括下
- 获取g0,若启用竞态分析则执行raceinit
- sched.maxmcount:限制调度器最多开启1万个线程
- tracebackinit:位于runtime/traceback.go,初始化traceback,在异常退出时打印调用栈
- moduledataverify:位于runtime/symtab.go,模块数据验证,读取section中的go内部定义数据
- stackinit:位于runtime/stack.go,负责goroutine栈池初始化,两个全局变量stackpool和stackLarge分别缓存小栈和大栈,取当前进程的堆内存
- mallocinit:位于runtime/malloc.go,负责内存分配器初始化,go的内存分配器实现类似tcmalloc,预定义了一组tinySize内存块,超过最大尺寸时取缓存,若无合适的缓存再取当前进程堆内存,垃圾回收负责释放这些内存
- mcommoninit:位于runtime/proc.go,如函数名一样,执行通用的m初始化,只不过这时初始化的是m0
- cpuinit:位于runtime/proc.go,获取CPU指令集特性
- alginit:位于runtime/alg.go,根据受支持的CPU指令集,初始化AES和Hash组件,由于map是哈希表,因此必须执行这个函数后才能使用map
- modulesinit:位于runtime/symtab.go,初始化所有已加载的模块
- typelinksinit:位于runtime/type.go,扫描所有加载的模块,构建模块数据的类型字典,以执行类型指针的去重
- itabsinit:位于runtime/iface.go,构建itabTable,初始化具体类型与interface类型的关联数据
- msigsave:位于runtime下所有os开头的go文件中,将当前线程的信号掩码存入m中
- goargs:位于runtime/runtime1.go文件中,格式化存储命令行参数
- goenvs:位于runtime下所有os开头的go文件中,格式化存储环境变量
- parsedebugvars:位于runtime/runtime1.go文件中,处理GODEBUG环境变量涉及的参数
- gcinit:位于runtime/mgc.go文件中,初始化垃圾回收器
- GOMAXPROCS:尝试读取环境变量GOMAXPROCS覆盖默认以读取到的ncpu,用于限制p数量
1// The bootstrap sequence is:
2//
3// call osinit
4// call schedinit
5// make & queue new G
6// call runtime·mstart
7//
8// The new G calls runtime·main.
9func schedinit() {
10 // raceinit must be the first call to race detector.
11 // In particular, it must be done before mallocinit below calls racemapshadow.
12 _g_ := getg()
13 if raceenabled {
14 _g_.racectx, raceprocctx0 = raceinit()
15 }
16
17 sched.maxmcount = 10000
18
19 tracebackinit()
20 moduledataverify()
21 stackinit()
22 mallocinit()
23 mcommoninit(_g_.m)
24 cpuinit() // must run before alginit
25 alginit() // maps must not be used before this call
26 modulesinit() // provides activeModules
27 typelinksinit() // uses maps, activeModules
28 itabsinit() // uses activeModules
29
30 msigsave(_g_.m)
31 initSigmask = _g_.m.sigmask
32
33 goargs()
34 goenvs()
35 parsedebugvars()
36 gcinit()
37
38 sched.lastpoll = uint64(nanotime())
39 procs := ncpu
40 if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
41 procs = n
42 }
43 if procresize(procs) != nil {
44 throw("unknown runnable goroutine during bootstrap")
45 }
46
47 // For cgocheck > 1, we turn on the write barrier at all times
48 // and check all pointer writes. We can't do this until after
49 // procresize because the write barrier needs a P.
50 if debug.cgocheck > 1 {
51 writeBarrier.cgo = true
52 writeBarrier.enabled = true
53 for _, p := range allp {
54 p.wbBuf.reset()
55 }
56 }
57
58 if buildVersion == "" {
59 // Condition should never trigger. This code just serves
60 // to ensure runtime·buildVersion is kept in the resulting binary.
61 buildVersion = "unknown"
62 }
63 if len(modinfo) == 1 {
64 // Condition should never trigger. This code just serves
65 // to ensure runtime·modinfo is kept in the resulting binary.
66 modinfo = ""
67 }
68}
3.8 runtime main函数
main不是程序的直接入口,更像是一个约定俗成的函数调用,在编译时被链接到指定位置,在Go中我们编写的main函数就会被链接到runtime/proc.go的func main_main(),runtime的main函数已经脱离了系统栈,位于m0的g队列的一个goroutine中,主要执行了以下操作
- racectx处理
- 根据指针长度(而不是CPU)限制最大栈长度,64位上最大1GB,32位上最大250 MB,以10进制计算
- 设置mainStarted标志位,允许newproc函数启动新的m
- 启动一个不绑定p的m,执行sysmon函数,该函数自动触发垃圾回收和netpoll
- 锁定主线程
- 执行所有runtime的init函数
- 启动垃圾回收
- 启动template线程
- cgo检查,确保cgo初始化成功
- 执行用户main函数及引用的所有包中的init函数
- 解锁主线程
- 执行用户main函数,等待退出
- 关闭静态检查
- panic检查
1//go:linkname runtime_inittask runtime..inittask
2var runtime_inittask initTask
3
4//go:linkname main_inittask main..inittask
5var main_inittask initTask
6
7// main_init_done is a signal used by cgocallbackg that initialization
8// has been completed. It is made before _cgo_notify_runtime_init_done,
9// so all cgo calls can rely on it existing. When main_init is complete,
10// it is closed, meaning cgocallbackg can reliably receive from it.
11var main_init_done chan bool
12
13//go:linkname main_main main.main
14func main_main()
15
16// mainStarted indicates that the main M has started.
17var mainStarted bool
18
19// runtimeInitTime is the nanotime() at which the runtime started.
20var runtimeInitTime int64
21
22// Value to use for signal mask for newly created M's.
23var initSigmask sigset
24
25// The main goroutine.
26func main() {
27 g := getg()
28
29 // Racectx of m0->g0 is used only as the parent of the main goroutine.
30 // It must not be used for anything else.
31 g.m.g0.racectx = 0
32
33 // Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
34 // Using decimal instead of binary GB and MB because
35 // they look nicer in the stack overflow failure message.
36 if sys.PtrSize == 8 {
37 maxstacksize = 1000000000
38 } else {
39 maxstacksize = 250000000
40 }
41
42 // Allow newproc to start new Ms.
43 mainStarted = true
44
45 if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
46 systemstack(func() {
47 newm(sysmon, nil)
48 })
49 }
50
51 // Lock the main goroutine onto this, the main OS thread,
52 // during initialization. Most programs won't care, but a few
53 // do require certain calls to be made by the main thread.
54 // Those can arrange for main.main to run in the main thread
55 // by calling runtime.LockOSThread during initialization
56 // to preserve the lock.
57 lockOSThread()
58
59 if g.m != &m0 {
60 throw("runtime.main not on m0")
61 }
62
63 doInit(&runtime_inittask) // must be before defer
64 if nanotime() == 0 {
65 throw("nanotime returning zero")
66 }
67
68 // Defer unlock so that runtime.Goexit during init does the unlock too.
69 needUnlock := true
70 defer func() {
71 if needUnlock {
72 unlockOSThread()
73 }
74 }()
75
76 // Record when the world started.
77 runtimeInitTime = nanotime()
78
79 gcenable()
80
81 main_init_done = make(chan bool)
82 if iscgo {
83 if _cgo_thread_start == nil {
84 throw("_cgo_thread_start missing")
85 }
86 if GOOS != "windows" {
87 if _cgo_setenv == nil {
88 throw("_cgo_setenv missing")
89 }
90 if _cgo_unsetenv == nil {
91 throw("_cgo_unsetenv missing")
92 }
93 }
94 if _cgo_notify_runtime_init_done == nil {
95 throw("_cgo_notify_runtime_init_done missing")
96 }
97 // Start the template thread in case we enter Go from
98 // a C-created thread and need to create a new thread.
99 startTemplateThread()
100 cgocall(_cgo_notify_runtime_init_done, nil)
101 }
102
103 doInit(&main_inittask)
104
105 close(main_init_done)
106
107 needUnlock = false
108 unlockOSThread()
109
110 if isarchive || islibrary {
111 // A program compiled with -buildmode=c-archive or c-shared
112 // has a main, but it is not executed.
113 return
114 }
115 fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
116 fn()
117 if raceenabled {
118 racefini()
119 }
120
121 // Make racy client program work: if panicking on
122 // another goroutine at the same time as main returns,
123 // let the other goroutine finish printing the panic trace.
124 // Once it does, it will exit. See issues 3934 and 20018.
125 if atomic.Load(&runningPanicDefers) != 0 {
126 // Running deferred functions should not take long.
127 for c := 0; c < 1000; c++ {
128 if atomic.Load(&runningPanicDefers) == 0 {
129 break
130 }
131 Gosched()
132 }
133 }
134 if atomic.Load(&panicking) != 0 {
135 gopark(nil, nil, waitReasonPanicWait, traceEvGoStop, 1)
136 }
137
138 exit(0)
139 for {
140 var x *int32
141 *x = 0
142 }
143}