Go程序是如何编译运行的
何为Runtime
简介:语言在运行的时候,作为支撑语言运行的部分。 也就是程序的运行环境
常见的runtime例如:
- Java:虚拟机
- JaveScript: 浏览器内核或者node的内核
Go的Runtime的特点:
- go是没有虚拟机的概念的
- Runtime 作为程序的一部分打包进二进制产物
- Runtime 随用户程序一起运行
- Runtime 与用户程序没有明显界限,直接通过函数调用
Go 的Runtime能力
- 内存管理能力(分配堆内存,栈内存,变量等)
- 垃圾回收能力(GC)
- 超强的并发能力(协程调度)
- 其他特点
-
Runtime有一定的屏蔽系统调用能力(go build详解)
-
一些go的关键字其实是Runtime下的函数:
-
总结:
Go 的 Runtime 负责内存管理,垃圾回收,协程调度 Go 的 Runtime 被编译成用户程序的一部分,一起运行
Go程序是如何编译的
使用命令 go build -n
可以输出程序的编译过程,并不实际编译
新建main.go文件
package main
import "fmt"
func main() {
fmt.Println("hello word")
}
在项目中并未引入runtime包,由此可以看出runtime是自动引入一起编译的。
先使用compile.exe
编译成 .a
文件 再通过link.exe
链接打包成目标文件 也就是 main.exe
Go 编译过程
词法分析
- 将源代码翻译成 Token
- Token 是代码中的最小语义结构
句法分析
- Token 序列经过处理,变成语法树
语义分析
- 类型检查
- 类型推断
- 查看类型是否匹配
- 函数调用内联
- 逃逸分析
中间码生成
- 为了处理不同平台的差异,先生成中间代码(SSA)
- 查看从代码到SSA中间码的整个过程
windows:$env:GOSSAFUNC="main" linux:export GOSSAFUNC=main
重新执行go build ,会生成 ssa.html
机器码生成
- 先生成Plan9汇编代码
- 最后编译成机器码
- 输出的机器码为.a文件
查看Plan9汇编代码
go build -gcflags -S main.go
链接
-
将各个包进行链接,包括runtime
Go程序是如何运行的
Go程序的入口
-
main方法就是入口吗?
-
runtime/rt0_xxx.s
-
读取命令行参数
- 复制参数数量 argc 和 和参数值 argv 到栈上
-
初始化 g0 执行栈
- g0 是为了调度协程而产生的协程
- g0 是每一个Go程序的第一个协程
-
运行时检测
- 检查各种类型的长度
- 检查指针操作
- 检查结构体字段偏移量
- 检查atomic原子操作
- 检查CAS操作
- 检查栈大小是否是2的幂
-
参数初始化 runtime.args
- 对命令行中的参数进行处理
- 参数数量赋值给 argc int32
- 参数值赋值给 argv **byte
-
调度器初始化 runtime.schedinit
- 全局栈空间内存分配
- 堆内存空间的初始化
- 初始化当前系统线程
- 算法初始化(map,hash)
- 加载命令行参数到 os.Args
- 加载操作系统环境变量
- 垃圾回收器的参数初始化
- 设置process数量
-
创建主协程
- 创建一个新的协程,执行runtime.main
- 放入调度器等待调度
-
初始化 M
- 初始化一个M 用来调度主协程
-
主协程执行主函数
- 执行 runtime 包中的 init 方法
- 启动 GC 垃圾收集器
- 执行用户包依赖的 init 方法
- 执行用户主函数 main.main()
Go SDK -> runtime -> rt0_xxx.s
就是Go程序真正的入口
例如:
window 64位的入口为 rt0_windows_amd64.s linux 64位的入口为 rt0_linux_amd64.s
在源码中不难发现 64位操作系统都会调用 asm_amd64.s中的_rt0_amd64(SB)
而这个方法主要是将 argc 和 argv 放进寄存器中,这两个是调用命令时候的参数 例如 :go build -n ...
之后调用 runtime.rt0_go
将参数放入栈中
创建一个 叫做 g0
的协程 (Go 语言运行是创建的第一个协程,并且不由调度器管理)
执行go语言的check方法
将 参数 copy 到go语言代码中 判断系统字长:调度器初始化需要用到 执行调度器初始化
获取runtime.main方法的地址,并启动一个新的协程用来执行该方法
初始化调度器m,使协程真正执行
runtime.proc.go -> 进入了go语言中的main方法
初始化一些东西
打开垃圾回收器
用户包依赖的初始化
在链接阶段会 链接到用户的 main方法
总结
- Go 启动时经历了检查、各种初始化、初始化协程调度的过程
- main.main() 也是在协程中运行的
问题
- 调度器是什么
- 为什么初始化M
- 为什么不是直接执行 main.main(),而是将其放入调度器
转载自:https://juejin.cn/post/7126725536088326174