0x00 golang 启动过程

 通过一段简单的demo来看下go 代码启动发生了啥

1
2
3
4
package main
func main(){

}

首先先定位到启动入口函数

从断点信息可以看到在linux amd64位操作系统上面的入口函数在/usr/lib/golang/src/runtime/rt0_linux_amd64.s:8这个里面。amd64表示在amd64位系统上的实现。当然这个在不通的平台上面有不同的实现,可以在源代码目录看到有各种架构结尾的文件。

从调试界面可以看到_rt0_amd64_linux 最终jmp到_rt0_amd64这个函数里面执行,而_rt0_amd64这个函数位于/usr/lib/golang/src/runtime/asm_amd64.s:15

打开对应的文件看下_rt0_amd64这个函数的实现,具体函数实现如下:

从注释里面可以看到这三行代码做的事情很简单了,将argc,argv这两个参数分别放到DISI这两个寄存器里面,然后在跳转到runtime·rt0_go(SB)这个地方去执行。

*runtime·rt0_go(SB)*函数
从调试界面可以看到rt0_go这个函数位于/usr/lib/golang/src/runtime/asm_amd64.s:89这个文件里面

具体代码如下

下面挨个看下rt0_go主要做什么。

1
2
3
4
5
6
7
// copy arguments forward on an even stack
MOVQ DI, AX // argc
MOVQ SI, BX // argv
SUBQ $(4*8+7), SP // 2args 2auto
ANDQ $~15, SP
MOVQ AX, 16(SP)
MOVQ BX, 24(SP)

这几行代码主要做的事情,先把两个参数的偏移地址放到ax,bx寄存器里面,然后扩充栈空间,并且四六字节对齐下,最后再把那两个参数argc,argv的偏移地址放到栈上面。代码注释可以大致了解这段代码主要是将命令行参数拷贝到栈上。

初始化栈信息
1
2
3
4
5
6
7
8
// create istack out of the given (operating system) stack.
// _cgo_init may update stackguard.
MOVQ $runtime·g0(SB), DI
LEAQ (-64*1024+104)(SP), BX
MOVQ BX, g_stackguard0(DI)
MOVQ BX, g_stackguard1(DI)
MOVQ BX, (g_stack+stack_lo)(DI)
MOVQ SP, (g_stack+stack_hi)(DI)

这段代码首先将g0的地址加载到di寄存器里面。然后将(sp-64*1024+107)(这个栈空间是干嘛的不清楚-,-)这个地方地址加载到BX里面,再将bx寄存器里面的值加载到g_stackguard0(DI)g_stackguard1(DI)以及(g_stack+stack_lo)(DI) 这三个变量里面,将SP寄存器里面的内容加载到(g_stack+stack_hi)(DI)变量里面,参考着注释,大概意思可能用用一段操作系统分配的栈来初始化当前g0的栈。

g0是一个全局变量,类型是g,可以从代码里面看到g的结构体:

获取CPU的信息
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
	// find out information about the processor we're on
MOVL $0, AX
CPUID
MOVL AX, SI
CMPL AX, $0
JE nocpuinfo

// Figure out how to serialize RDTSC.
// On Intel processors LFENCE is enough. AMD requires MFENCE.
// Don't know about the rest, so let's do MFENCE.
CMPL BX, $0x756E6547 // "Genu"
JNE notintel
CMPL DX, $0x49656E69 // "ineI"
JNE notintel
CMPL CX, $0x6C65746E // "ntel"
JNE notintel
MOVB $1, runtime·isIntel(SB)
MOVB $1, runtime·lfenceBeforeRdtsc(SB)
notintel:

// Load EAX=1 cpuid flags
MOVL $1, AX
CPUID
MOVL AX, runtime·processorVersionInfo(SB)

这段代码主要是获取CPU信息的,CPUID(这个玩意应该plan9提供的一个汇编指令),(盲猜cpuid这个指令获取的数据存放在bx,dx,cx这三个寄存器里面),下面三个CMPL 指令主要ax,bx,cx三个寄存器组成的值(GenuineIntel)和GenuineIntel做对比,如果有一个不匹配的跳过

初始化cgo
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

nocpuinfo:
// if there is an _cgo_init, call it.
MOVQ _cgo_init(SB), AX
TESTQ AX, AX
JZ needtls
// arg 1: g0, already in DI
MOVQ $setg_gcc<>(SB), SI // arg 2: setg_gcc
#ifdef GOOS_android
MOVQ $runtime·tls_g(SB), DX // arg 3: &tls_g
// arg 4: TLS base, stored in slot 0 (Android's TLS_SLOT_SELF).
// Compensate for tls_g (+16).
MOVQ -16(TLS), CX
#else
MOVQ $0, DX // arg 3, 4: not used when using platform's TLS
MOVQ $0, CX
#endif
#ifdef GOOS_windows
// Adjust for the Win64 calling convention.
MOVQ CX, R9 // arg 4
MOVQ DX, R8 // arg 3
MOVQ SI, DX // arg 2
MOVQ DI, CX // arg 1
#endif
CALL AX

// update stackguard after _cgo_init
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)

#ifndef GOOS_windows
JMP ok
#endif
needtls:

这一整段都是设置cgo相关的,从代码注释里面可以看到,只有当启用cgo的时候才会调用这段代码,否则直接跳转到needtls这段里面继续执行,(cgo? 从来没用过,这段代码拜拜)

设置tls

 代码的165-181主要是判断操作系统是否支持tls,如果匹配到对应的操作系统(也就这些操作系统不支持TLS),那么则跳过tls相关的设置。

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
needtls:
#ifdef GOOS_plan9
// skip TLS setup on Plan 9
JMP ok
#endif
#ifdef GOOS_solaris
// skip TLS setup on Solaris
JMP ok
#endif
#ifdef GOOS_illumos
// skip TLS setup on illumos
JMP ok
#endif
#ifdef GOOS_darwin
// skip TLS setup on Darwin
JMP ok
#endif

LEAQ runtime·m0+m_tls(SB), DI
CALL runtime·settls(SB)

// store through it, to make sure it works
get_tls(BX)
MOVQ $0x123, g(BX)
MOVQ runtime·m0+m_tls(SB), AX
CMPQ AX, $0x123
JEQ 2(PC)
CALL runtime·abort(SB)

tls的设置对应着代码的182-192行
先看下183-184行这两行代码

1
2
LEAQ	runtime·m0+m_tls(SB), DI
CALL runtime·settls(SB)

这段代码先将m0+m_tls的地址加载到,di寄存器里面,然后在调用runtime.settls
首先runtime.m0+m_tls这个地址代表啥意思?和上面g0一样,m0也是一个全局变量,这里的代码意思就是将m0这个变量的tls这个字段的地址加载到di寄存器里面。可以在源代码里面看到m0这个变量对应的类型是m

下面我们继续看下settls这个函数里面具体干了啥,首选我们需要找到这段代码的位置这段函数在sys_linux_amd64.s:TEXT runtime·settls(SB),NOSPLIT,$32 里面。代码如下:

从注释里面看,这段代码(666行)位FS寄存器设置一个偏移地址(0x1002),然后执行一个系统调用(SYS_arch_prctl), 然后这$0xfffffffffffff001这个里面的值对比,如果不匹配则crash掉,($0xfffffffffffff001这段值干嘛的我也不知道-,-)
int arch_prctl(int code, unsigned long addr)这代码应该调用这个函数——-(有段有点迷糊,有懂得大佬可以指点下)
但是从反汇编的代码可以看到

以及对应的AT&T汇编代码

基本可以猜测这段汇编代码对应的就是 MOVQ (TLS), CX这段指令。

tls校验

继续返回主流程

1
2
3
4
5
6
get_tls(BX)
MOVQ $0x123, g(BX) ; 将 0x123放到g里面
MOVQ runtime·m0+m_tls(SB), AX ; 将tls的信息存放到AX里面
CMPQ AX, $0x123 ; 将AX(也就是之前初始化tls的信息)和0x123做对比
JEQ 2(PC)
CALL runtime·abort(SB)

这段代码应该是对tls做校验, 0x123是测试数据,主要验证tls是否work起来

初始化TLS, 并进行双向绑定

绑定是g0和m0进行绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ok:
// set the per-goroutine and per-mach "registers"
// 获取tls的信息
get_tls(BX)
// 加载g0的地址到cx寄存器里面,
LEAQ runtime·g0(SB), CX
// g里面的值是0x123, cx是g0的地址,
// 将cx的值放到g里面====这个就是用g0来填充tls里面信息,
// 之前0x123是测试数据,到了这里就需要用g0来填充了
MOVQ CX, g(BX)
// 加载m0的地址到AX里面
LEAQ runtime·m0(SB), AX

// 将g0赋值给m_g0
// save m->g0 = g0
MOVQ CX, m_g0(AX)
// save m0 to g0->m
// 将m0赋值给go->m
MOVQ AX, g_m(CX)

这段代码应该就是做双向绑定, 首先在初始化tls之后,用g0信息来填充TLS
将m0->g0 = g0, g0->m = m0

校验

1
2
3

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

剩余

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

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)
// 调用runtime.args 主要做参数校验的
CALL runtime·args(SB)
// os初始化
CALL runtime·osinit(SB)
// scheduler初始化
CALL runtime·schedinit(SB)

// create a new goroutine to start program
// 将main.pc放到ax里面
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
PUSHQ $0 // arg size
// 创建一个新的goroutine
CALL runtime·newproc(SB)
POPQ AX
POPQ AX

// start this M
// 启动m(goroutime是调度到m上才会执行)
CALL runtime·mstart(SB)

CALL runtime·abort(SB) // mstart should never return
RET

// Prevent dead-code elimination of debugCallV1, which is
// intended to be called by debuggers.
MOVQ $runtime·debugCallV1(SB), AX

汇编看着头大,os初始化,scheduler后续在慢慢看

总结

 由于这篇文章起源于群里面一个问题当运行main.main()整个流程是怎么用的。
现在简单回答下:当启动一个go程序的时候,大概的流程如下几件事情:

  • 先为g0初始化栈空间
  • 保存命令参数,获取cpu信息(盲猜应该是用于设置GOMAXPROCS的)
  • 设置tls,并且双向绑定m0(即将m0和g0双向绑定)
  • 对参数进行校验
  • 初始化系统信息(内存/垃圾回收……)
  • 初始化调度器
  • 创建一个新的goroutine(用于运行用户的代码)
  • 启动m

引用

http://www.hechaku.com/unix_linux/arch_prctl.html
https://golang.org/doc/asm
https://studygolang.com/articles/2917
https://www.luozhiyun.com/archives/448