本文使用 golang 1.17 代码,如有任何问题,还望指出。

Golang 代码被操作系统运行起来的流程

一、编译

go 源代码首先要通过 go build 编译为可执行文件,在 linux 平台上为 ELF 格式的可执行文件,编译阶段会经过编译器、汇编器、链接器三个过程最终生成可执行文件。



二、运行

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

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

Golang 程序启动流程分析

1、通过 gdb 调试分析程序启动流程

此处以一个简单的 go 程序通过单步调试来分析其启动过程的流程:

main.go

编译该程序并使用 gdb 进行调试。使用 gdb 调试时首先在程序入口处设置一个断点,然后进行单步调试即可看到该程序启动过程中的代码执行流程。

runtime/rt0_linux_amd64.sCALL runtime·mstart(SB)

启动流程流程中的函数调用如下所示:

2、golang 启动流程分析

上节通过gdb调试已经看到了 golang 程序在启动过程中会执行一系列的汇编指令,本节会具体分析启动程序过程中每条指令的含义,了解了这些才能明白 golang 程序在启动过程中所执行的操作。

src/runtime/rt0_linux_amd64.s
JMP _rt0_amd64_rt0_amd64src/runtime/asm_amd64.s
_rt0_amd64rt0_gort0_go
runtime·argsruntime·osinitruntime·schedinitruntime·newprocruntime·mstart

执行完以上指令后,进程内存空间布局如下所示:



然后开始执行获取 cpu 信息的指令以及与 cgo 初始化相关的,此段代码暂时可以不用关注。

needtls
getggetg

tls 地址会写到 m0 中,而 m0 会和 g0 绑定,所以可以直接从 tls 中获取到 g0。

继续执行 ok 代码块,主要逻辑为:

runtime·osinitruntime·schedinitruntime·newprocruntime·mstart

此时进程内存空间布局如下所示:



查看 ELF 二进制文件结构

可以通过 readelf 命令查看 ELF 二进制文件的结构,可以看到二进制文件中代码区和数据区的内容,全局变量保存在数据区,函数保存在代码区。

总结

本文主要介绍 Golang 程序启动流程中的关键代码,启动过程的主要代码是通过 Plan9 汇编编写的,如果没有做过底层相关的东西看起来还是非常吃力的,笔者对其中的一些细节也未完全搞懂,如果有兴趣可以私下讨论一些详细的实现细节,其中有一些硬编码的数字以及操作系统和硬件相关的规范理解起来相对比较困难。针对 Golang runtime 中的几大组件也会陆续写出相关的分析文章。

参考: