Golang 的启动过程分析

2017, Sep 30    

系统环境

系统版本: Fedora Release 25
内核版本:4.12.8-200.fc25.x86_64
GO: 1.10.1 linux/amd64

从二进制中查找 Entry Point

首先编译一个 go 的二进制程序(此处我编译的是 golang/dep 项目),然后使用 objdump 或者 readelf 取得程序的入口地址:

$ objdump -f dep

dep:     file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x00000000004581d0

可以看到启动地址为 0x0000000000454420(地址不一定和此处相同)。
然后对程序进行反汇编:

$ objdump -d dep > dep.asm

然后使用任意文本工具查看 dep.asm,并找到 4581d0 地址出的汇编:

  454420:       48 8b 3c 24             mov    (%rsp),%rdi
  454424:       48 8d 74 24 08          lea    0x8(%rsp),%rsi
  454429:       e9 02 00 00 00          jmpq   454430 <_cgo_topofstack@@Base-0x2e50>
   ...
  454430:       48 89 f8                mov    %rdi,%rax
  454433:       48 89 f3                mov    %rsi,%rbx
  454436:       48 83 ec 27             sub    $0x27,%rsp
  45443a:       48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
  45443e:       48 89 44 24 10          mov    %rax,0x10(%rsp)
  454443:       48 89 5c 24 18          mov    %rbx,0x18(%rsp)
  454448:       48 8d 3d 91 41 85 00    lea    0x854191(%rip),%rdi        # ca85e0 <sigismember@plt+0x425410>
  45444f:       48 8d 9c 24 68 00 ff    lea    -0xff98(%rsp),%rbx
  ...
  4581d0:       e9 4b c2 ff ff          jmpq   454420 <_cgo_topofstack@@Base-0x2e60>

入口点 4581d0 对应的函数位于:runtime/rt0_linux_amd64.s#7

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
	JMP	_rt0_amd64(SB)

这个函数直接跳转到了 _rt0_amd64:runtime/asm_amd64.s#14

TEXT _rt0_amd64(SB),NOSPLIT,$-8
	MOVQ	0(SP), DI	// argc
	LEAQ	8(SP), SI	// argv
	JMP	runtime·rt0_go(SB)

_rt0_amd64 只是把 argc 和 argv 放入到 rdi 和 rsi 寄存器,然后调用 rt0_go 函数:runtime/asm_amd64.s#87

TEXT runtime·rt0_go(SB),NOSPLIT,$0
	// 此处将 rsp -= 39,然后按照 16 字节对齐,此时栈上至少有 39 字节可用。
	// 然后将 argc 和 argv 复制到 rsp+16 和 rsp+24 的位置。
	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)

此时栈布局如下

+----------------------+
|     Stack Layout     |
+----------------------+
| ...                  |
| argc                 |
| argv                 |
| envp                 |
| ...                  |
| argv pointers        |
| NULL                 |
| environment pointers |
| NULL                 |
| ELF Auxiliary Table  |
| argv strings         |
| environment strings  |
| program name         |
| NULL                 |
+----------------------+ <--- 以上部分是 linux 启动进程时填充的
| ...                  |
| argv (rsp+24)        |
| argc (rsp+16)        |
| (Not Set) (rsp+8)    |
| (Not Set) (rsp+0)    |
+----------------------+ <--- rsp

初始化 g0,g0 的栈实际上就是 linux 分配的栈。g0 占用了大约 64k 的大小。

	// runtime.g0 位于 runtime/proc.go#80 
	// g0.stackguard0 =  rsp-64*1024+104
	// g0.stackguard1 = g0.stackguard0
	// g0.stack.lo = g0.stackguard0
	// g0.stack.hi = rsp
	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)

此处之后是一段探测 CPU 和 指令集的代码,忽略。

如果启用了 cgo,则会对 cgo 进行初始化:

	// 检查是否存在 _cgo_init 函数,如果有就执行
	MOVQ	_cgo_init(SB), AX
	TESTQ	AX, AX
	JZ	needtls
	// 这里的 DI 就是上面初始化 g0 时设置的 g0 的地址
	MOVQ	DI, CX	// Win64 uses CX for first parameter
	MOVQ	$setg_gcc<>(SB), SI
	CALL	AX

	// _cgo_init 初始化完成之后,重新设置 g0 的 stack 守卫。
	MOVQ	$runtime·g0(SB), CX
	MOVQ	(g_stack+stack_lo)(CX), AX
	// _StackGuard 位于 runtime/stack.go#93
	ADDQ	$const__StackGuard, AX
	MOVQ	AX, g_stackguard0(CX)
	MOVQ	AX, g_stackguard1(CX)

如果没有启用 cgo,则会初始化 Thread local storage:

needtls:
	// runtime.m0 位于 runtime/proc.go#79
	LEAQ	runtime·m0+m_tls(SB), DI
	// settls 位于 runtime/sys_linux_amd64.s#601
	CALL	runtime·settls(SB)

	// get_tls 和 g 是宏,位于 runtime/go_tls.h#10
	// #define	get_tls(r)	MOVQ TLS, r
	// #define	g(r)	0(r)(TLS*1)
	// 此处对 tls 进行了一次测试,确保值正确写入了 m0.tls
	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 里,这里实际上就是 m0.tls
	get_tls(BX)
	LEAQ	runtime·g0(SB), CX
	MOVQ	CX, g(BX)
	LEAQ	runtime·m0(SB), AX

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

	CLD
	// check 位于 runtime/runtime1.go#136
	// 这个函数检查了各种类型以及类型转换是否有问题
	CALL	runtime·check(SB)

	// 将 argc 和 argv 移动到 rsp+0 和 rsp+8 的位置,模拟函数调用时对参数的 push
	// 此处完成了 args 的分析,os 初始化,调度器初始化。
	MOVL	16(SP), AX		// copy argc
	MOVL	AX, 0(SP)
	MOVQ	24(SP), AX		// copy argv
	MOVQ	AX, 8(SP)
	// args 位于 runtime/runtime1.go#60
	// args 会去 stack 里读取参数和环境变量以及 Auxiliary Table
	CALL	runtime·args(SB)
	// osinit 位于 runtime/os_linux.go#272
	// osinit 初始化 cpu 数量
	CALL	runtime·osinit(SB)
	// schedinit 位于 runtime/proc.go#477
	// schedinit 初始化调度器,内存,参数,环境变量,gc
	// schedinit 初始化根据 cpu 数量和 GOMAXPROCS 确定需要的 p 的数量,
	// 然后将 m0 的 p 设置为创建的第一个 p
	CALL	runtime·schedinit(SB)

	// 获取 runtime.main 的地址,调用 newproc 创建 p
	MOVQ	$runtime·mainPC(SB), AX
	PUSHQ	AX
	PUSHQ	$0			// arg size
	// newproc 位于 runtime/proc.go#3240
	// newproc 创建一个新的 g 并放置到等待队列里
	CALL	runtime·newproc(SB)
	POPQ	AX
	POPQ	AX

	// mstart 位于 runtime/proc.go#1175
	// mstart 会调用 schedule 函数进入调度状态
	CALL	runtime·mstart(SB)

	MOVL	$0xf1, 0xf1  // crash
	RET

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