0x00 golang 启动过程 通过一段简单的demo来看下go 代码启动发生了啥
1 2 3 4 package mainfunc 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这两个参数分别放到DI和SI这两个寄存器里面,然后在跳转到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
引用