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

经上述几个步骤生成可执行文件后,二进制文件在被操作系统加载起来运行时会通过如下几个阶段:

  1. 从磁盘上把可执行程序读入内存;
  2. 创立过程和主线程;
  3. 为主线程调配栈空间;
  4. 把由用户在命令行输出的参数拷贝到主线程的栈;
  5. 把主线程放入操作系统的运行队列期待被调度执起来运行;

START_THREAD(elf_ex, regs, elf_entry, bprm->p) 启动线程传入了 elf_entry 参数,这是程序的入口地址。

这个 elf_entry 被写在 elf 可执行文件的 header 中

$ readelf -h mainELF 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 外围:

  1. schedinit:进行各种运行时组件初始化工作,这包含咱们的调度器与内存分配器、回收器的初始化
  2. newproc:负责依据主 goroutine(即 main)入口地址创立可被运行时调度的执行单元,这里的main还不是用户的main函数,是 runtime.main
  3. 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.gofunc 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, 次要进行了

  1. 启动零碎后盾监控sysmon 线程
  2. 执行 runtime 包内 init
  3. 启动gc
  4. 用户包依赖 init 的执行
  5. 执行用户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_gort0_go 中 先进行了 go 程序的 runtime 的初始化,其中包含:调度器,栈,堆内存空间初始化,垃圾回收器的初始化,最初最初通过newprocmstart调度执行runtime.main,实现一系列初始化过程,再而后才是执行用户的主函数。

参考

  1. https://www.bookstack.cn/read...
  2. https://eddycjy.com/posts/go/...
  3. https://loulan.me/post/golang...
  4. https://juejin.cn/post/694250...