1. Go语言是如何编译的

1. Go 程序是如何编译的

1.1 什么是 runtime?

runtime 就是程序的运行环境

  • java:java 虚拟机
  • javaStript:浏览器内核

Go 语言的 runtime:

  1. Go 语言没有虚拟机这个概念
  2. Go 的 runtime 作为程序的一部分被打包到二进制文件中
  3. runtime 也会随用户程序一起运行
  4. runtime 与用户程序没有明显的界限,直接通过函数调用

runtime 的能力:

  1. 内存管理能力
  2. 垃圾回收能力(GC)
  3. 超强的并发能力(协程调度)

runtime 的其他特点:

  1. runtime 有一定的屏蔽系统调用能力
  2. 一些 Go 的关键字其实是 runtime 下的函数

总结:

  1. Go 的 Runtime 负责内存管理、垃圾回收、协程调度
  2. G o的 Runtime 被编译为用户程序的一部分,一起运行

1.2 Go 是如何编译的?

例如如下程序:

package main

import "fmt"

func main() {
	fmt.Println("hello world")
}
  • 使用 go build -n 命令查看编译过程:

1.3 Go 的编译过程

  1. 词法分析:将源代码翻译成 Token,token 是代码中的最小语义结构
  2. 句法分析:将 token 处理后编程语法树如下:
image-20220622150717197
  1. 语义分析:类型检查、类型推断、查看类型是否匹配,函数调用内联、逃逸分析

  2. 中间码生成(SSA):为了处理不同平台的差异,先生成中间代码

    • 查看中间代码的整个过程
    $env:GOOSFUNC="main"   # windows
    # export GOOSAFUNC=main  # linux
    go build
    
  3. 代码优化:每一步其实都可能进行代码优化

  4. 机器码生成:

    • 先生成 Plan9 汇编代码(查看 Plan9 汇编代码:go build -gcflags -S main.go
    • 最后编译为机器码
    • 输出的机器码为 .a 文件

  5. 链接:将各个包进行链接,包括 runtime

总结:

1.4 Go 程序运行过程

Go 程序的入口是什么?

Go 语言的程序入口其实在 runtime 包下,该包下有很多 rt_..._...s 文件,如下:

  • rt0:runtime,0 表示 runtime 的入口
  • linux:表示操作系统
  • amd64:芯片架构

Go 语言运行的时候,就会先运行电脑对应的方法

我们可以查看 _rt0_amd64(SB) 方法,在 \runtime\asm_amd64.s 文件中:

  • 该方法将 argc 和 argv 存放到寄存器中
  • argc 和 argv 表示调用命令时跟的参数,例如使用 go build -n,-n 就是参数
  • 调用 runtime.rt0_go(SB)
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)  // 1. 将参数放到栈上

	// create istack out of the given (operating system) stack.
	// _cgo_init may update stackguard.
	// 2. 初始化一个叫 g0 的协程,g0 是为了调度协程而产生的协程,g0 是go程序的第一个协程
	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)

...

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)  // 3. 运行时检测,这是一个go语言的方法,在runtime1.go中(检查各类型的长度,指针操作、结构体字段的偏移量,atomic原子操作,CAS操作,栈大小是否是2的次幂)

	// 4. 将 argc 和 argv 拷贝到 go 代码中,参数的数量赋值给 argc int32,参数的值赋给 argv **byte
	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)  // 5. 判断系统的字长和CPU的核数
	CALL	runtime·schedinit(SB)  // 6. 调度器初始化(全局栈空间内存分配、加载命令行参数到os.Args、堆内存空间的初始化、加载操作系统环境变量、初始化当前系统线程、垃圾回收期的参数初始化、算法初始化map/hash、设置process数量)

	// create a new goroutine to start program
	// 7. 取出runtime.main方法的地址,并不是我们自己写的main方法的地址
	MOVQ	$runtime·mainPC(SB), AX		// entry
	PUSHQ	AX
	PUSHQ	$0			// arg size
	CALL	runtime·newproc(SB)  // 8. 启动一个新协程
	POPQ	AX
	POPQ	AX

	// start this M
	CALL	runtime·mstart(SB)  // 9. 初始化调度器的M,我们新建的协程就会真正的执行

	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

目前为止,我们的系统已经开启了两个协程(g0 和运行 runtime.main 的协程->主协程),将 runtime.main 放入调度器等待调度

初始化 M,用来调度主协程

主协程执行执行主函数

查看 runtime.main,在 runtime/proc.go 中

// The main goroutine.
func main() {
    
    doInit(&runtime_inittask) // Must be before defer.  1. 执行 runtime 包中的 init 方法

	// Defer unlock so that runtime.Goexit during init does the unlock too.
	needUnlock := true
	defer func() {
		if needUnlock {
			unlockOSThread()
		}
	}()

	gcenable()  // 2. 启动垃圾回收
    
    ...
    
    doInit(&main_inittask)  // 3. 执行用户包依赖的 init 方法
    
	...
    // 4. 这里就是调用我们自己的main方法
	fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
    ...
}
  • 查看 main_main
//go:linkname main_main main.main
func main_main()
  • 链接到我们自己的 main 方法

总结:主协程执行主函数

  1. 执行 runtime 包中的 init 方法
  2. 启动 GC 垃圾收集器
  3. 执行用户包依赖的 init 方法
  4. 执行用户主函数 main.main()
posted @ 2022-06-25 11:34  Reed-Luo  阅读(217)  评论(0)    收藏  举报