go 二进制启动流程分析
gdb 分析 go 二进制的启动流程
通过简单的 go 文件 start.go
分析 go 二进制启动流程。
package main
import "fmt"
func main() {
fmt.Print("test go binary")
}
gdb 分析步骤:
- 关闭内联函数和编译优化配置后,对 go 源码进行编译
- 通过 info files 找到 go 二进制进程的入口
- 在入口地址打上断点
- 单步运行
具体调试过程如下所示。
root@ubuntu-hirsute:~/go/src/go-tracing/gobinary#
>> go build -a -gcflags "-N -l" -o start start.go
root@ubuntu-hirsute:~/go/src/go-tracing/gobinary#
>> gdb start
GNU gdb (Ubuntu 10.1-2ubuntu2) 10.1.90.20210411-git
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from start...
Loading Go Runtime support.
(gdb) info files
Symbols from "/root/go/src/go-tracing/gobinary/start".
Local exec file:
`/root/go/src/go-tracing/gobinary/start', file type elf64-x86-64.
Entry point: 0x454dc0
0x0000000000401000 - 0x000000000048cfa3 is .text
0x000000000048d000 - 0x00000000004dc5f0 is .rodata
0x00000000004dc7c0 - 0x00000000004dd42c is .typelink
0x00000000004dd430 - 0x00000000004dd480 is .itablink
0x00000000004dd480 - 0x00000000004dd480 is .gosymtab
0x00000000004dd480 - 0x0000000000548999 is .gopclntab
0x0000000000549000 - 0x0000000000549020 is .go.buildinfo
0x0000000000549020 - 0x00000000005560f8 is .noptrdata
0x0000000000556100 - 0x000000000055d150 is .data
0x000000000055d160 - 0x00000000005789d0 is .bss
0x00000000005789e0 - 0x000000000057b148 is .noptrbss
0x0000000000400f9c - 0x0000000000401000 is .note.go.buildid
(gdb) b *(0x454dc0)
Breakpoint 1 at 0x454dc0: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
(gdb) r
Starting program: /root/go/src/go-tracing/gobinary/start
Breakpoint 1, _rt0_amd64_linux () at /usr/local/go/src/runtime/rt0_linux_amd64.s:8
8 JMP _rt0_amd64(SB)
(gdb)
_rt0_amd64 () at /usr/local/go/src/runtime/asm_amd64.s:15
15 MOVQ 0(SP), DI // argc
(gdb)
16 LEAQ 8(SP), SI // argv
(gdb)
17 JMP runtime·rt0_go(SB)
(gdb)
runtime.rt0_go () at /usr/local/go/src/runtime/asm_amd64.s:89
89 MOVQ DI, AX // argc
(gdb)
90 MOVQ SI, BX // argv
(gdb)
91 SUBQ $(4*8+7), SP // 2args 2auto
(gdb)
runtime.rt0_go () at /usr/local/go/src/runtime/asm_amd64.s:92
92 ANDQ $~15, SP
(gdb)
runtime.rt0_go () at /usr/local/go/src/runtime/asm_amd64.s:93
93 MOVQ AX, 16(SP)
(gdb)
94 MOVQ BX, 24(SP)
(gdb)
98 MOVQ $runtime·g0(SB), DI
(gdb)
99 LEAQ (-64*1024+104)(SP), BX
(gdb)
100 MOVQ BX, g_stackguard0(DI)
(gdb)
101 MOVQ BX, g_stackguard1(DI)
(gdb)
102 MOVQ BX, (g_stack+stack_lo)(DI)
(gdb)
103 MOVQ SP, (g_stack+stack_hi)(DI)
(gdb)
106 MOVL $0, AX
(gdb)
107 CPUID
(gdb)
109 CMPL AX, $0
(gdb)
110 JE nocpuinfo
(gdb)
115 CMPL BX, $0x756E6547 // "Genu"
(gdb)
116 JNE notintel
(gdb)
117 CMPL DX, $0x49656E69 // "ineI"
(gdb)
118 JNE notintel
(gdb)
119 CMPL CX, $0x6C65746E // "ntel"
(gdb)
120 JNE notintel
(gdb)
121 MOVB $1, runtime·isIntel(SB)
(gdb)
122 MOVB $1, runtime·lfenceBeforeRdtsc(SB)
(gdb)
126 MOVL $1, AX
(gdb)
127 CPUID
(gdb)
132 MOVQ _cgo_init(SB), AX
(gdb)
133 TESTQ AX, AX
(gdb)
134 JZ needtls
(gdb)
183 LEAQ runtime·m0+m_tls(SB), DI
(gdb)
184 CALL runtime·settls(SB)
(gdb)
188 MOVQ $0x123, g(BX)
(gdb)
189 MOVQ runtime·m0+m_tls(SB), AX
(gdb)
190 CMPQ AX, $0x123
(gdb)
191 JEQ 2(PC)
(gdb)
196 LEAQ runtime·g0(SB), CX
(gdb)
197 MOVQ CX, g(BX)
(gdb)
198 LEAQ runtime·m0(SB), AX
(gdb)
201 MOVQ CX, m_g0(AX)
(gdb)
203 MOVQ AX, g_m(CX)
(gdb)
205 CLD // convention is D is always left cleared
(gdb)
206 CALL runtime·check(SB)
(gdb)
208 MOVL 16(SP), AX // copy argc
(gdb)
209 MOVL AX, 0(SP)
(gdb)
210 MOVQ 24(SP), AX // copy argv
(gdb)
211 MOVQ AX, 8(SP)
(gdb)
212 CALL runtime·args(SB)
(gdb)
213 CALL runtime·osinit(SB)
(gdb)
214 CALL runtime·schedinit(SB)
(gdb)
217 MOVQ $runtime·mainPC(SB), AX // entry
(gdb)
218 PUSHQ AX
(gdb)
219 PUSHQ $0 // arg size
(gdb)
220 CALL runtime·newproc(SB)
(gdb)
221 POPQ AX
(gdb)
222 POPQ AX
(gdb)
225 CALL runtime·mstart(SB)
(gdb)
[New LWP 1239316]
[New LWP 1239317]
[New LWP 1239319]
[New LWP 1239318]
test go binary[LWP 1239319 exited]
[LWP 1239317 exited]
[LWP 1239316 exited]
[LWP 1238518 exited]
[Inferior 1 (process 1238518) exited normally]
(gdb)
通过上述的初步调试可以发现,go 的二进制进程启动的过程大致是:
_rt0_amd64 -->
rt0_go -->
runtime·settls -->
runtime·check -->
runtime·args -->
runtime·osinit -->
runtime·schedinit -->
runtime·newproc -->
runtime·mstart -->
main.main(fmt.Print("test go binary"))
go 二进制启动流程详细分析
以 go 1.17 版本的源码进行进程启动的详细分析,golang 源码参考链接:Github golang 1.17。
启动步骤1
gdb 调试信息:
(gdb) b *(0x454dc0)
Breakpoint 1 at 0x454dc0: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
源码信息:
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "textflag.h"
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
JMP _rt0_amd64_lib(SB)
由入口点跳转到对应平台的执行函数例如 amd64 平台就是 _rt0_amd64
。
启动步骤2
gdb 调试信息:
(gdb)
_rt0_amd64 () at /usr/local/go/src/runtime/asm_amd64.s:15
15 MOVQ 0(SP), DI // argc
(gdb)
16 LEAQ 8(SP), SI // argv
(gdb)
17 JMP runtime·rt0_go(SB)
源码信息:
// _rt0_amd64 is common startup code for most amd64 systems when using
// internal linking. This is the entry point for the program from the
// kernel for an ordinary -buildmode=exe program. The stack holds the
// number of arguments and the C-style argv.
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(进程启动的参数,指针类型)
- argc 参数被保存到寄存器 DI
- argv 参数被保存到寄存器 SI
- 执行函数
rt0_go
函数
启动步骤3
go 二进程主要的进程准备逻辑都是在 runtime.rt0_go
函数中完成的,下面我打算将 rt0_go 函数按照逻辑拆分来分析。
启动步骤3.1
gdb 调试信息:
(gdb)
runtime.rt0_go () at /usr/local/go/src/runtime/asm_amd64.s:89
89 MOVQ DI, AX // argc
(gdb)
90 MOVQ SI, BX // argv
(gdb)
91 SUBQ $(4*8+7), SP // 2args 2auto
(gdb)
runtime.rt0_go () at /usr/local/go/src/runtime/asm_amd64.s:92
92 ANDQ $~15, SP
(gdb)
runtime.rt0_go () at /usr/local/go/src/runtime/asm_amd64.s:93
93 MOVQ AX, 16(SP)
(gdb)
94 MOVQ BX, 24(SP)
源码信息:
TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
// 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)
这一步主要是处理命令行参数,即将命令行参数拷贝到主线程的栈上。主要包括:
- argc 拷贝到 AX 寄存器
- argv 拷贝到 BX 寄存器
- 将栈扩大为 39 字节(这里不是很理解为什么是 39 字节)
- 进行16字节对齐
- 将 argc 放到栈指针 SP + 16 字节处
- 将 argv 放到栈指针 SP + 24 字节处
启动步骤3.2
gdb 调试信息:
(gdb)
98 MOVQ $runtime·g0(SB), DI
(gdb)
99 LEAQ (-64*1024+104)(SP), BX
(gdb)
100 MOVQ BX, g_stackguard0(DI)
(gdb)
101 MOVQ BX, g_stackguard1(DI)
(gdb)
102 MOVQ BX, (g_stack+stack_lo)(DI)
(gdb)
103 MOVQ SP, (g_stack+stack_hi)(DI)
源码信息:
// 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,为 g0 在主线程栈上分配大约 64K 栈空间,并设置 g0 的 stackguard0,stackguard1,stack 三个字段:
- g0 的地址放入 DI 寄存器
- 设置主线程栈空间
BX = SP - 64*1024 + 104
- 开始初始化 g0 对象的 stackguard0,stackguard1,stack 这三个字段
g0.stackguard0 = SP - 64*1024 + 104
g0.stackguard1 = SP - 64*1024 + 104
g0.stack.lo = SP - 64*1024 + 104
g0.stack.hi = SP
启动步骤3.3
gdb 调试信息:
(gdb)
106 MOVL $0, AX
(gdb)
107 CPUID
(gdb)
109 CMPL AX, $0
(gdb)
110 JE nocpuinfo
(gdb)
115 CMPL BX, $0x756E6547 // "Genu"
(gdb)
116 JNE notintel
(gdb)
117 CMPL DX, $0x49656E69 // "ineI"
(gdb)
118 JNE notintel
(gdb)
119 CMPL CX, $0x6C65746E // "ntel"
(gdb)
120 JNE notintel
(gdb)
121 MOVB $1, runtime·isIntel(SB)
(gdb)
122 MOVB $1, runtime·lfenceBeforeRdtsc(SB)
(gdb)
126 MOVL $1, AX
(gdb)
127 CPUID
(gdb)
132 MOVQ _cgo_init(SB), AX
(gdb)
133 TESTQ AX, AX
源码信息:
// 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)
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
这一步骤主要是执行 CPUID 指令,探测 CPU 信息和指令集代码,然后再执行 nocpuinfo 代码块判断是否需要初始化 cgo,如果开启了 cgo 特性,则会修改 g0 的部分字段。
启动步骤3.4
gdb 调试信息:
(gdb)
133 TESTQ AX, AX
(gdb)
134 JZ needtls
(gdb)
183 LEAQ runtime·m0+m_tls(SB), DI
(gdb)
184 CALL runtime·settls(SB)
(gdb)
188 MOVQ $0x123, g(BX)
(gdb)
189 MOVQ runtime·m0+m_tls(SB), AX
(gdb)
190 CMPQ AX, $0x123
(gdb)
191 JEQ 2(PC)
源码信息:
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
#ifdef GOOS_openbsd
// skip TLS setup on OpenBSD
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)
这一步骤执行 needtls 代码块,初始化 tls 和 m0,tls 为线程本地存储,在 golang 程序运行过程中,每个 m 都需要和一个工作线程关联,那么工作线程如何知道其关联的 m,此时就会用到线程本地存储,线程本地存储就是线程私有的全局变量,通过线程本地存储可以为每个线程初始化一个私有的全局变量 m,然后就可以在每个工作线程中都使用相同的全局变量名来访问不同的 m 结构体对象。后面会分析到其实每个工作线程 m 在刚刚被创建出来进入调度循环之前就利用线程本地存储机制为该工作线程实现了一个指向 m 结构体实例对象的私有全局变量。tls 地址会写到 m0 中,而 m0 会和 g0 绑定,所以可以直接从 tls 中获取到 g0。
在后面代码分析中,会经常看到调用 getg 函数,getg 函数会从线程本地存储中获取当前正在运行的 g,这里获取出来的 m 关联的 g0。
NOTE:由于示例代码中没有用到本地变量,所以 tls 代码部分调试信息比较少,这里查阅资料做一下补充,如下所示。
// 下面开始初始化tls(thread local storage,线程本地存储),设置 m0 为线程私有变量,将 m0 绑定到主线程
needtls:
LEAQ runtime·m0+m_tls(SB), DI // DI = &m0.tls,取m0的tls成员的地址到DI寄存器
// 调用 runtime·settls 函数设置线程本地存储,runtime·settls 函数的参数在 DI 寄存器中
// 在 runtime·settls 函数中将 m0.tls[1] 的地址设置为 tls 的地址
// runtime·settls 函数在 runtime/sys_linux_amd64.s#599
CALL runtime·settls(SB)
// 此处是在验证本地存储是否可以正常工作,确保值正确写入了 m0.tls,
// 如果有问题则 abort 退出程序
// get_tls 是宏,位于 runtime/go_tls.h
get_tls(BX) // 将 tls 的地址放入 BX 中,即 BX = &m0.tls[1]
MOVQ $0x123, g(BX) // BX = 0x123,即 m0.tls[0] = 0x123
MOVQ runtime·m0+m_tls(SB), AX // AX = m0.tls[0]
CMPQ AX, $0x123
JEQ 2(PC) // 如果相等则向后跳转两条指令即到 ok 代码块
CALL runtime·abort(SB) // 使用 INT 指令执行中断
启动步骤3.5
gdb 调试信息:
(gdb)
196 LEAQ runtime·g0(SB), CX
(gdb)
197 MOVQ CX, g(BX)
(gdb)
198 LEAQ runtime·m0(SB), AX
(gdb)
201 MOVQ CX, m_g0(AX)
(gdb)
203 MOVQ AX, g_m(CX)
(gdb)
205 CLD // convention is D is always left cleared
(gdb)
206 CALL runtime·check(SB)
(gdb)
208 MOVL 16(SP), AX // copy argc
(gdb)
209 MOVL AX, 0(SP)
(gdb)
210 MOVQ 24(SP), AX // copy argv
(gdb)
211 MOVQ AX, 8(SP)
(gdb)
212 CALL runtime·args(SB)
(gdb)
213 CALL runtime·osinit(SB)
(gdb)
214 CALL runtime·schedinit(SB)
(gdb)
217 MOVQ $runtime·mainPC(SB), AX // entry
(gdb)
218 PUSHQ AX
(gdb)
219 PUSHQ $0 // arg size
(gdb)
220 CALL runtime·newproc(SB)
(gdb)
221 POPQ AX
(gdb)
222 POPQ AX
(gdb)
225 CALL runtime·mstart(SB)
源码信息:
ok:
// set the per-goroutine and per-mach "registers"
get_tls(BX)
LEAQ runtime·g0(SB), CX
MOVQ CX, g(BX)
LEAQ runtime·m0(SB), AX
// save m->g0 = g0
MOVQ CX, m_g0(AX)
// save m0 to g0->m
MOVQ AX, g_m(CX)
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)
CALL runtime·args(SB)
CALL runtime·osinit(SB)
CALL runtime·schedinit(SB)
// create a new goroutine to start program
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
PUSHQ $0 // arg size
CALL runtime·newproc(SB)
POPQ AX
POPQ AX
// start this M
CALL runtime·mstart(SB)
CALL runtime·abort(SB) // mstart should never return
RET
// Prevent dead-code elimination of debugCallV2, which is
// intended to be called by debuggers.
MOVQ $runtime·debugCallV2<ABIInternal>(SB), AX
RET
// 下面是摘取的 Reference 中的注释版本,帮助理解细节:
// 首先将 g0 地址保存在 tls 中,即 m0.tls[0] = &g0,然后将 m0 和 g0 绑定
// 即 m0.g0 = g0, g0.m = m0
ok:
get_tls(BX) // 获取tls地址到BX寄存器,即 BX = m0.tls[0]
LEAQ runtime·g0(SB), CX // CX = &g0
MOVQ CX, g(BX) // m0.tls[0]=&g0
LEAQ runtime·m0(SB), AX // AX = &m0
MOVQ CX, m_g0(AX) // m0.g0 = g0
MOVQ AX, g_m(CX) // g0.m = m0
CLD // convention is D is always left cleared
// check 函数检查了各种类型以及类型转换是否有问题,位于 runtime/runtime1.go#137 中
CALL runtime·check(SB)
// 将 argc 和 argv 移动到 SP+0 和 SP+8 的位置
// 此处是为了将 argc 和 argv 作为 runtime·args 函数的参数
MOVL 16(SP), AX
MOVL AX, 0(SP)
MOVQ 24(SP), AX
MOVQ AX, 8(SP)
// args 函数会从栈中读取参数和环境变量等进行处理
// args 函数位于 runtime/runtime1.go#61
CALL runtime·args(SB)
// osinit 函数用来初始化 cpu 数量,函数位于 runtime/os_linux.go#301
CALL runtime·osinit(SB)
// schedinit 函数用来初始化调度器,函数位于 runtime/proc.go#654
CALL runtime·schedinit(SB)
// 创建第一个 goroutine 执行 runtime.main 函数。获取 runtime.main 的地址,调用 newproc 创建 g
MOVQ $runtime·mainPC(SB), AX
PUSHQ AX // runtime.main 作为 newproc 的第二个参数入栈
PUSHQ $0 // newproc 的第一个参数入栈,该参数表示runtime.main函数需要的参数大小,runtime.main没有参数,所以这里是0
// newproc 创建一个新的 goroutine 并放置到等待队列里,该 goroutine 会执行runtime.main 函数, 函数位于 runtime/proc.go#4250
CALL runtime·newproc(SB)
// 弹出栈顶的数据
POPQ AX
POPQ AX
// mstart 函数会启动主线程进入调度循环,然后运行刚刚创建的 goroutine,mstart 会阻塞住,除非函数退出,mstart 函数位于 runtime/proc.go#1328
CALL runtime·mstart(SB)
CALL runtime·abort(SB) // mstart should never return
RET
// Prevent dead-code elimination of debugCallV2, which is
// intended to be called by debuggers.
MOVQ $runtime·debugCallV2<ABIInternal>(SB), AX
RET
执行 ok 代码块,主要逻辑为:
- 将 m0 和 g0 进行绑定,启动主线程;
- 调用 runtime·osinit 函数用来初始化 cpu 数量,调度器初始化时需要知道当前系统有多少个 CPU 核;
- 调用 runtime·schedinit 函数会初始化 m0 和 p 对象,还设置了全局变量 sched 的 maxmcount 成员为10000,限制最多可以创建 10000 个操作系统线程出来工作;
- 调用 runtime·newproc 为 main 函数创建 goroutine;
- 调用 runtime·mstart 启动主线程,执行 main 函数;
至此 go 二进制就启动完成了,下面就开始运行 main 函数的逻辑了。最后为了帮助更好的理解,我引用了 Reference 中的 go 二进程的内存空间布局图: