共计 3215 个字符,预计需要花费 9 分钟才能阅读完成。
go run main.go
一个 Go 程序就启动了。然而这背地操作系统如何执行到 Go 代码的,Go 为了运行用户 main 函数,又做了什么?
一 编译
go build main.go
咱们写的 go 代码都是编译成可执行文件去机器上间接执行的,在 linux 平台上是 ELF 格局的可执行文件,linux 能间接执行这个文件。
- 编译器:将 go 代码生成 .s 汇编代码,go 中应用的是 plan9 汇编
- 汇编起:将汇编代码转成机器代码,即目标程序 .o 文件
- 链接器:将多个 .o 文件合并链接失去最终可执行文件
graph LR
0(写代码)--go 程序 --> 1(编译器)-- 汇编代码 --> 2(汇编器)--.o 目标程序 -->3(链接器)-- 可执行文件 -->4(完结)
二 操作系统加载
./main
经上述几个步骤生成可执行文件后,二进制文件在被操作系统加载起来运行时会通过如下几个阶段:
- 从磁盘上把可执行程序读入内存;
- 创立过程和主线程;
- 为主线程调配栈空间;
- 把由用户在命令行输出的参数拷贝到主线程的栈;
- 把主线程放入操作系统的运行队列期待被调度执起来运行;
START_THREAD(elf_ex, regs, elf_entry, bprm->p)
启动线程传入了 elf_entry
参数,这是程序的入口地址。
这个 elf_entry
被写在 elf 可执行文件的 header 中
$ readelf -h main
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x45d430
Start of program headers: 64 (bytes into file)
Start of section headers: 456 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 7
Size of section headers: 64 (bytes)
Number of section headers: 25
Section header string table index: 3
并且通过反编译 可执行文件,能够看到这个地址对应的就是 _rt0_amd64_linux。
$ objdump ./main -D > tmp
$ grep tmp '45d430'
000000000045d430 <_rt0_amd64_linux>:
45d430: e9 2b c4 ff ff jmpq 459860 <_rt0_amd64>
从此进入了 Go 程序的启动过程
Go 程序启动
Go 程序启动地位, 把栈上的入口参数存到寄存器中,接下来跳转到 rt0_go
启动函数
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB)
rt0_go
代码比拟长,可分为两个局部,第一局部是零碎参数获取和运行时查看。第二局部是 go 程序启动的外围,这里只具体介绍第二局部,总体启动流程如下
go runtime 外围:
schedinit
:进行各种运行时组件初始化工作,这包含咱们的调度器与内存分配器、回收器的初始化newproc
:负责依据主 goroutine(即 main)入口地址创立可被运行时调度的执行单元,这里的 main 还不是用户的 main 函数,是runtime.main
mstart
:开始启动调度器的调度循环, 执行队列中 入口办法是runtime.main
的 G
TEXT runtime·rt0_go(SB),NOSPLIT,$0
(...)
// 调度器初始化
CALL runtime·schedinit(SB)
// 创立一个新的 goroutine 来启动程序
MOVQ $runtime·mainPC(SB), AX
PUSHQ AX
PUSHQ $0 // 参数大小
CALL runtime·newproc(SB)
POPQ AX
POPQ AX
// 启动这个 M,mstart 应该永不返回
CALL runtime·mstart(SB)
(...)
RET
shedinit
包含了所有外围组件的初始化工作
// src/runtime/proc.go
func schedinit() {_g_ := getg()
(...)
// 栈、内存分配器、调度器相干初始化
sched.maxmcount = 10000 // 限度最大零碎线程数量
stackinit() // 初始化执行栈
mallocinit() // 初始化内存分配器
mcommoninit(_g_.m) // 初始化以后零碎线程
(...)
gcinit() // 垃圾回收器初始化
(...)
// 创立 P
// 通过 CPU 外围数和 GOMAXPROCS 环境变量确定 P 的数量
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {procs = n}
procresize(procs)
(...)
}
执行 runtime.main
, 次要进行了
- 启动零碎后盾监控 sysmon 线程
- 执行 runtime 包内 init
- 启动 gc
- 用户包依赖 init 的执行
- 执行用户 main.mian 办法
// The main goroutine.
func main() {g := getg()
...
// 执行栈最大限度:1GB(64 位零碎)或者 250MB(32 位零碎)if sys.PtrSize == 8 {maxstacksize = 1000000000} else {maxstacksize = 250000000}
...
// 启动零碎后盾监控(定期垃圾回收、抢占调度等等)systemstack(func() {newm(sysmon, nil)
})
...
// 让 goroute 独占以后线程,// runtime.lockOSThread 的用法详见 http://xiaorui.cc/archives/5320
lockOSThread()
...
// runtime 包外部的 init 函数执行
runtime_init() // must be before defer
// Defer unlock so that runtime.Goexit during init does the unlock too.
needUnlock := true
defer func() {
if needUnlock {unlockOSThread()
}
}()
// 启动 GC
gcenable()
...
// 用户包的 init 执行
main_init()
...
needUnlock = false
unlockOSThread()
...
// 执行用户的 main 主函数
main_main()
...
// 退出
exit(0)
for {
var x *int32
*x = 0
}
}
总结
启动一个 Go 程序时,首先要通过操作系统的加载,通过可执行文件中 Entry point address
记录的地址,找到 go 程序启动入口: _rt0_amd64
-> rt0_go
。rt0_go
中 先进行了 go 程序的 runtime 的初始化,其中包含: 调度器,栈,堆内存空间初始化,垃圾回收器的初始化,最初最初通过 newproc
和mstart
调度执行runtime.main
, 实现一系列初始化过程,再而后才是执行用户的主函数。
参考
- https://www.bookstack.cn/read…
- https://eddycjy.com/posts/go/…
- https://loulan.me/post/golang…
- https://juejin.cn/post/694250…