引导过程是了解Go运行时如何工作的关键。如果您想继续使用Go,学习它是必不可少的。因此,我们的Golang Internals系列的第五部分专门讨论Go运行时,尤其是Go引导过程。这次您将了解:
- 自举
- 可调整大小的堆栈实现
- 内部TLS实施
请注意,这篇文章包含许多汇编代码,您至少需要一些基础知识才能继续(这里是Go汇编程序的快速指南)。
寻找一个切入点
首先,我们需要找到启动Go程序后立即执行的功能。为此,我们将编写一个简单的Go应用。
package main
func main() {
print(123)
}
然后,我们需要对其进行编译和链接。
go tool compile -N -l -S main.go
6.out
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.txt42f160
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
起始顺序
现在,我们需要在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_linuxargcargvDISISPruntime.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)
AXBX
// 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.g0runtime,ggoroutineruntime.g0goroutinegoroutinestack.lostack.higoroutinestackguard0stackguard1runtime.rt0_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
CXruntime.ggoroutineruntime.gstackguard0
runtime.morestack_noctxtstackguard1stackguard0runtime.morestack_noctxt
Go自举的进一步调查
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:
runtime·lfenceBeforeRdtscruntime·cputickscpu ticksruntime·lfenceBeforeRdtscruntime·cpuid_ecxruntime·cpuid_edx
好的,让我们继续检查代码的另一部分。
// 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。现在,是时候了解它是如何实现的了
内部TLS实施
如果仔细看一下前面的代码片段,您将很容易理解实际工作中仅有的几行。
LEAQ runtime·tls0(SB), DI
CALL runtime·settls(SB)
runtime·tls0runtime·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_prctlARCH_SET_FSFSruntime·tls0
您还记得我们在主函数的汇编代码开头看到的指令吗?
0x0000 00000 (test.go:3) MOVQ (TLS),CX
runtime.ggoroutinedisassembly.txtmain.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)
runtime·g0runtime.m0runtime.g0goroutineruntime.m0goroutineruntime.g0runtime.m0
开始序列的最后一部分将初始化参数并调用不同的函数,但这是单独讨论的主题。因此,我们了解了引导过程的内部机制,并了解了如何实现堆栈。为了前进,我们需要分析开始序列的最后一部分,这将是我们下一篇博客文章的主题。