引导过程是了解Go运行时如何工做的关键。若是您想继续使用Go,学习它是必不可少的。所以,咱们的Golang Internals系列的第五部分专门讨论Go运行时,尤为是Go引导过程。此次您将了解:html
请注意,这篇文章包含许多汇编代码,您至少须要一些基础知识才能继续(这里是Go汇编程序的快速指南)。linux
首先,咱们须要找到启动Go程序后当即执行的功能。为此,咱们将编写一个简单的Go应用。git
package main func main() { print(123) }
而后,咱们须要对其进行编译和连接。github
go tool compile -N -l -S main.go
这将6.out
在您的当前目录中建立一个名为的可执行文件。下一步涉及objdump工具,该工具特定于Linux。Windows和Mac用户能够找到相似物或彻底跳过此步骤。如今,运行如下命令。golang
objdump -f 6.out
您应该得到输出,其中将包含起始地址。算法
6.out: file format elf64-x86-64 architecture: i386:x86-64, flags 0x00000112: EXEC_P, HAS_SYMS, D_PAGED start address 0x000000000042f160
接下来,咱们须要反汇编可执行文件,并找到哪一个函数位于该地址。编程
objdump -d 6.out > disassemble.txt
而后,咱们须要打开disassemble.txt
文件并搜索42f160
。咱们获得的输出以下所示windows
000000000042f160 <_rt0_amd64_linux>: 42f160: 48 8d 74 24 08 lea 0x8(%rsp),%rsi 42f165: 48 8b 3c 24 mov (%rsp),%rdi 42f169: 48 8d 05 10 00 00 00 lea 0x10(%rip),%rax # 42f180 <main> 42f170: ff e0 jmpq *%rax
很好,咱们找到了!个人操做系统和体系结构的入口点是一个名为的函数_rt0_amd64_linux
。sass
如今,咱们须要在Go运行时源中找到此函数。它位于rt0_linux_amd64.s文件中。若是查看Go运行时程序包,则能够找到许多带有与操做系统和体系结构名称相关的后缀的文件名。构建运行时程序包时,仅选择与当前OS和体系结构相对应的文件。其他的被跳过。让咱们仔细看一下rt0_linux_amd64.s。函数
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8 LEAQ 8(SP), SI // argv MOVQ 0(SP), DI // argc MOVQ $main(SB), AX JMP AX TEXT main(SB),NOSPLIT,$-8 MOVQ $runtime·rt0_go(SB), AX JMP AX
该_rt0_amd64_linux
功能是很是简单的。它调用main函数并将参数(argc
和argv
)保存在寄存器(DI
和SI
)中。参数位于堆栈中,而且能够经过SP
(堆栈指针)寄存器进行访问。主要功能也很简单。它调用的runtime.rt0_go
函数更长且更复杂,所以咱们将其分红小部分并分别描述。第一部分是这样的。
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
减小堆栈指针。咱们还为另外两个四字节变量添加了空间,并将其调整为16位对齐。最后,咱们将参数移回堆栈。
// 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)
第二部分比较棘手。首先,咱们将全局runtime.g0
变量的地址加载到DI寄存器中。此变量在proc1.go文件中定义,而且属于该runtime,g
类型。将为goroutine
系统中的每一个变量建立此类型的变量。如您所料,runtime.g0
描述了root goroutine
。而后,咱们初始化描述root堆栈的字段goroutine
。的意义stack.lo
和stack.hi
应当明确。这些是指向current的堆栈开始和结束的指针goroutine
,可是stackguard0
andstackguard1
字段是什么?为了理解这一点,咱们须要搁置对该runtime.rt0_go
函数的研究,并仔细研究Go中的堆栈增加。
Go语言使用可调整大小的堆栈。每个都goroutine
从一个小的堆栈开始,而且每当达到某个阈值时,它的大小就会更改。显然,有一种方法能够检查咱们是否已达到此阈值。实际上,检查是在每一个功能的开始执行的。为了了解它的工做原理,让咱们用该-S
标志再编译一次示例程序(这将显示生成的汇编代码)。主要功能的开始看起来像这样。
"".main t=1 size=48 value=0 args=0x0 locals=0x8 0x0000 00000 (test.go:3) TEXT "".main+0(SB),$8-0 0x0000 00000 (test.go:3) MOVQ (TLS),CX 0x0009 00009 (test.go:3) CMPQ SP,16(CX) 0x000d 00013 (test.go:3) JHI ,22 0x000f 00015 (test.go:3) CALL ,runtime.morestack_noctxt(SB) 0x0014 00020 (test.go:3) JMP ,0 0x0016 00022 (test.go:3) SUBQ $8,SP
首先,咱们将值从线程本地存储(TLS)加载到CX
寄存器(咱们已经在上一篇文章中解释了TLS是什么)。此值始终包含一个指向runtime.g
与current对应的结构的指针goroutine
。而后,咱们将堆栈指针与runtime.g
结构中位于16个字节偏移处的值进行比较。咱们能够轻松地计算出这对应于该stackguard0
字段。
所以,这就是咱们检查是否已达到堆栈阈值的方式。若是还没有达到,则检查失败。在这种状况下,咱们将runtime.morestack_noctxt
反复调用该函数,直到为堆栈分配了足够的内存为止。该stackguard1
字段的工做方式与极为类似stackguard0
,可是它在C堆栈增加序言中使用,而不是在Go中使用。的内部运做runtime.morestack_noctxt
方式也是一个很是有趣的话题,但咱们将在稍后进行讨论。如今,让咱们回到引导过程。
咱们将经过查看runtime.rt0_go
函数中代码的下一部分来开始启动序列。
// find out information about the processor we're on MOVQ $0, AX CPUID CMPQ 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·lfenceBeforeRdtsc(SB) notintel: MOVQ $1, AX CPUID MOVL CX, runtime·cpuid_ecx(SB) MOVL DX, runtime·cpuid_edx(SB) nocpuinfo:
这部分对于理解Go的主要概念不是相当重要的,所以咱们将对其进行简要介绍。在这里,咱们试图找出正在使用的处理器。若是是Intel,则设置runtime·lfenceBeforeRdtsc
变量。该runtime·cputicks
方法是惟一使用此变量的地方。此方法利用不一样的汇编器指令来获取cpu ticks
依赖于的值runtime·lfenceBeforeRdtsc
。最后,咱们调用CPUID汇编程序指令,执行该指令,而后将结果保存在runtime·cpuid_ecx
和runtime·cpuid_edx
变量中。这些在alg.go文件中使用,以选择计算机体系结构自己支持的适当哈希算法。
好的,让咱们继续检查代码的另外一部分。
// if there is an _cgo_init, call it. MOVQ _cgo_init(SB), AX TESTQ AX, AX JZ needtls // g0 already in DI MOVQ DI, CX // Win64 uses CX for first parameter MOVQ $setg_gcc<>(SB), SI 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) CMPL runtime·iswindows(SB), $0 JEQ ok
该片断仅在cgo
启用时执行。
下一个代码片断负责设置TLS。
needtls: // skip TLS setup on Plan 9 CMPL runtime·isplan9(SB), $1 JEQ ok // skip TLS setup on Solaris CMPL runtime·issolaris(SB), $1 JEQ ok LEAQ runtime·tls0(SB), DI CALL runtime·settls(SB) // store through it, to make sure it works get_tls(BX) MOVQ $0x123, g(BX) MOVQ runtime·tls0(SB), AX CMPQ AX, $0x123 JEQ 2(PC) MOVL AX, 0 // abort
咱们以前已经提到过TLS。如今,是时候了解它是如何实现的了
若是仔细看一下前面的代码片断,您将很容易理解实际工做中仅有的几行。
LEAQ runtime·tls0(SB), DI CALL runtime·settls(SB)
当您的操做系统不支持TLS设置时,全部其余全部内容都将用于跳过TLS设置,并检查TLS是否正常工做。上面的两行将runtime·tls0
变量的地址存储在DI寄存器中并调用该runtime·settls
函数。该功能的代码以下所示。
// set tls base to DI TEXT runtime·settls(SB),NOSPLIT,$32 ADDQ $8, DI // ELF wants to use -8(FS) MOVQ DI, SI MOVQ $0x1002, DI // ARCH_SET_FS MOVQ $158, AX // arch_prctl SYSCALL CMPQ AX, $0xfffffffffffff001 JLS 2(PC) MOVL $0xf1, 0xf1 // crash RET
从注释中,咱们能够了解到此函数进行arch_prctl
系统调用并ARCH_SET_FS
做为参数传递。咱们还能够看到,该系统调用为FS
段寄存器设置了基础。在咱们的例子中,咱们将TLS设置为指向runtime·tls0
变量。
您还记得咱们在主函数的汇编代码开头看到的指令吗?
0x0000 00000 (test.go:3) MOVQ (TLS),CX
前面咱们已经解释过,它将runtime.g
结构实例的地址加载到CX寄存器中。此结构描述了当前结构,goroutine
并存储在线程本地存储中。如今,咱们能够找到并了解如何将此指令转换为机器汇编程序。若是打开先前建立的disassembly.txt
文件并查找该main.main
函数,则其中的第一条指令应以下所示。
400c00: 64 48 8b 0c 25 f0 ff mov %fs:0xfffffffffffffff0,%rcx
本指令(%fs:0xfffffffffffffff0
)中的冒号表明分段寻址(您能够在本教程中阅读更多内容)。
最后,让咱们看一下runtime.rt0_go
函数的最后两部分。
// 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)
在这里,咱们将TLS地址加载到BX寄存器中,并将runtime·g0
变量的地址保存在TLS中。咱们还初始化runtime.m0
变量。若是runtime.g0
表明root goroutine
,则runtime.m0
对应于用于运行root的根操做系统线程goroutine
。咱们可能须要仔细看看runtime.g0
和runtime.m0
结构在即将到来的博客文章。
开始序列的最后一部分将初始化参数并调用不一样的函数,但这是单独讨论的主题。所以,咱们了解了引导过程的内部机制,并了解了如何实现堆栈。为了前进,咱们须要分析开始序列的最后一部分,这将是咱们下一篇博客文章的主题。