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信息不再赘述,更多的具体资料如下:

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.gofunc 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}