教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 Go的隐秘世界:Go程序的启动和runtime初始化

Go的隐秘世界:Go程序的启动和runtime初始化

发布时间:2022-03-02   编辑:jiaochengji.com
教程集为您提供Go的隐秘世界:Go程序的启动和runtime初始化等资源,欢迎您收藏本站,我们将为您提供最新的Go的隐秘世界:Go程序的启动和runtime初始化资源

书接上文:Go的隐秘世界:一个Goroutine要几个Thread

上文提到:一个 Go 程序启动的时候就会启动多个线程,而不是像一个C/C 程序那样只有一个线程用来执行 main 函数。

启动线程的是谁呢?是 Go runtime。什么是 runtime 呢?任何一种高级语言都提供给用户写程序的形式。比如 C 和 Go 的”主程序“都是一个叫 main 的函数。那么是谁调用的用户写的 main 函数呢?—— 就是这种语言的 runtime。

这 runtime 为啥有机会调用用户写的 main 函数呢?这是因为高级语言编译器在把用户写的程序翻译成可执行文件的过程中,把 runtime 代码塞进了可执行文件,而且在文件头中的 entrypoint field 的值设置成了 runtime 里的某个函数的起始地址。比如,GCC 在编译 C 程序的时候会把 libgcc.a 的内容塞进可执行文件里。这个 libgcc.a 也就是 GCC 编译器的 C runtime。它的功能很简单:(1)初始化全局变量,(2)调用用户写的 main 函数。

Go 的 runtime 也需要初始化全局变量,还需要调用每个 module 里定义的 init 函数,还需要初始化 GC,以及初始化 Go scheduler,启动一个 goroutine,并且让这个 goroutine 执行用户定义的 main 函数 —— 是为 Go runtime 的初始化。

当我们运行一个 Go 程序的时候,操作系统 load 可执行文件,并且开始读取文件头里的 entrypoint field 指向的 CPU 指令并且执行之 —— 是为 Go 程序的启动。

用户启动 Go 程序;操作系统执行 runtime 里的入口函数;runtime 执行初始化过程,最后调用用户写的 main 函数 —— 这个过程,就是本文要分析的主要过程。

我们的分析通过在 Linux 上反汇编一个 Go 程序,来回溯这个启动过程。如果你想复现本文中的操作过程,手边又没有 Linux 电脑,可以在 macOS 或者 Windows 上安装一个虚拟机软件,比如 VirtualBox,或者安装 Docker。后者在启动一个 Docker container 时会偷摸地启动一个 Linux 虚拟机。

我们就用最常见的 Hello World 程序吧。

<pre><code class="lang-go hljs"><span class="kn">package</span> <span class="nx">main</span> <span class="kn">import</span> <span class="p">(</span> <span class="s">"fmt"</span> <span class="p">)</span> <span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">"hello world"</span><span class="p">)</span> <span class="p">}</span> </code></code></pre>

首先我们编译这个程序 a.go 到可执行文件 a:

<pre><code class="lang-bash hljs">go build -o a a.go</code></code></pre>

我们在 Linux 下执行上述命令,得到的可执行文件 a 是 Linux 的 ELF 可执行文件格式。用 Linux 里的 readelf 命令,我们可以打印 ELF 的文件头,其中有执行这个文件时第一条指令所在的位置,也就是 a 的入口地址(entrypoint)。

<pre><code class="lang-text hljs">root@7f2187b3c225:/go# readelf -h /tmp/a | grep -i entry Entry point address: 0x4645e0</code></code></pre>

接下来,我们要看看这个入口地址 0x4645e0 指向的汇编程序。为此,我们用 objdump 命令反汇编 a,得到 a.S

<pre><code class="lang-nasm hljs"><span class="nf">objdump</span> <span class="o">-</span><span class="nv">S</span> <span class="nv">a</span> <span class="o">></span> <span class="nv">a.S</span></code></code></pre>

用文本编辑器打开 a.S,然后搜索入口地址”4645e0“,我们找到以下代码。

<pre><code class="lang-nasm hljs"><span class="err">00000000004645</span><span class="nf">e0</span> <span class="o"><</span><span class="nv">_rt0_amd64_linux</span><span class="o">></span><span class="p">:</span> <span class="err">//</span> <span class="nf">Copyright</span> <span class="mi">2009</span> <span class="nv">The</span> <span class="nv">Go</span> <span class="nv">Authors.</span> <span class="nb">Al</span><span class="nv">l</span> <span class="nv">rights</span> <span class="nv">reserved.</span> <span class="err">4645</span><span class="nl">e0:</span> <span class="nf">e9</span> <span class="mb">1b</span> <span class="nv">cb</span> <span class="nv">ff</span> <span class="nv">ff</span> <span class="nv">jmpq</span> <span class="mi">461100</span> <span class="o"><</span><span class="nv">_rt0_amd64</span><span class="o">></span> <span class="err">4645</span><span class="nl">e5:</span> <span class="nf">cc</span> <span class="nv">int3</span> </code></code></pre>

可以看出来,这个入口是一个函数,叫做 _rt0_amd64_linux。根据下面的注释,可以看到这个入口函数是 Go 编译器生成的。它只有一行指令,跳转到一个叫 _rt0_amd64 的函数。这个函数定义位于 Go runtime 里,源码在 golang/go 。

<pre><code class="lang-text hljs">TEXT _rt0_amd64(SB),NOSPLIT,$-8 MOVQ 0(SP), DI // argc LEAQ 8(SP), SI // argv JMP runtime·rt0_go(SB)</code></code></pre>

因为我是在 AMD64 系统上用 Linux 做上述实验的,所以这个函数所在的文件名是 runtime/asm_amd64.s 。同一个目录下,有其他汇编源码文件,分别对应其他 CPU 体系结构,包括 ARM、PowerPC、MIPS 和 WASM(Web assembly)。

上述函数很简单,只是调用了 Go runtime 里另一个函数 rt0_go。这个函数也在同一个汇编源码文件里。这个汇编函数定义略长,我们贴一个 GitHub permalink:

<span class="LinkCard-backdrop" style="background-image:url(https://picb.zhimg.com/v2-5d5330f6d421401028228973eaee6d74_ipico.jpg)"/><span class="LinkCard-content"><span class="LinkCard-text"><span class="LinkCard-title" data-text="true">golang/go</span><span class="LinkCard-meta"><span style="display:inline-flex;align-items:center">​<svg class="Zi Zi--InsertLink" fill="currentColor" viewbox="0 0 24 24" width="17" height="17"><path d="M13.414 4.222a4.5 4.5 0 1 1 6.364 6.364l-3.005 3.005a.5.5 0 0 1-.707 0l-.707-.707a.5.5 0 0 1 0-.707l3.005-3.005a2.5 2.5 0 1 0-3.536-3.536l-3.005 3.005a.5.5 0 0 1-.707 0l-.707-.707a.5.5 0 0 1 0-.707l3.005-3.005zm-6.187 6.187a.5.5 0 0 1 .638-.058l.07.058.706.707a.5.5 0 0 1 .058.638l-.058.07-3.005 3.004a2.5 2.5 0 0 0 3.405 3.658l.13-.122 3.006-3.005a.5.5 0 0 1 .638-.058l.069.058.707.707a.5.5 0 0 1 .058.638l-.058.069-3.005 3.005a4.5 4.5 0 0 1-6.524-6.196l.16-.168 3.005-3.005zm8.132-3.182a.25.25 0 0 1 .353 0l1.061 1.06a.25.25 0 0 1 0 .354l-8.132 8.132a.25.25 0 0 1-.353 0l-1.061-1.06a.25.25 0 0 1 0-.354l8.132-8.132z"/></svg></span>github.com</span></span><span class="LinkCard-imageCell"></span></span>

从这个函数的源码,大家可以看到 Go 源码库里的汇编程序是用的 Plan 9 汇编器的语法写的。这个语法为了兼容各种 CPU 体系结构,有一定的抽象,所以并不一定每一条汇编指令都一一对应到 CPU 指令。不过这些不妨碍我们阅读代码,实际上简化了代码阅读。另外,虽然 Go 使用的汇编语法是 Plan 9 的,但是汇编器是自己实现的,并没有复用 Plan 9 的汇编器。更多关于 Go 的汇编语言的细节,可以看 <span class="invisible">https://</span><span class="visible">golang.org/doc/asm</span><span class="invisible"/> 。不过目前我们并不需要追溯这些细节。只需要注意,一个汇编函数以 TEXT directive 开头,以 RET 指令(或者其他一条跳转指令)结束。

这个 rt0_go 函数具体做了以下几件事情:

<ol><li>调用 x_cgo_init 函数。</li><li>调用 runtime.osinit 函数。</li><li>调用 schedinit 函数。</li><li>创建 run queue 和一个新的 G(goroutine)。</li><li>调用 runtime·mstart 函数。</li></ol>

在接下来的文章里,我们要深入分析 rt0_go 这个函数到底做了什么。不过,为了大家看 runtime 代码看的明白,我们先得说说代码的设计思想,尤其是 Go runtime 的主要内容 Go scheduler 的设计思想,否则至少上面 4. 里提到的 run queue 是啥 —— 读者就懵了。那我又是怎么知道这些设计思想的呢?我看了设计文档

<span class="LinkCard-content"><span class="LinkCard-text"><span class="LinkCard-title" data-text="true">Scalable Go Scheduler Design Doc</span><span class="LinkCard-meta"><span style="display:inline-flex;align-items:center">​<svg class="Zi Zi--InsertLink" fill="currentColor" viewbox="0 0 24 24" width="17" height="17"><path d="M13.414 4.222a4.5 4.5 0 1 1 6.364 6.364l-3.005 3.005a.5.5 0 0 1-.707 0l-.707-.707a.5.5 0 0 1 0-.707l3.005-3.005a2.5 2.5 0 1 0-3.536-3.536l-3.005 3.005a.5.5 0 0 1-.707 0l-.707-.707a.5.5 0 0 1 0-.707l3.005-3.005zm-6.187 6.187a.5.5 0 0 1 .638-.058l.07.058.706.707a.5.5 0 0 1 .058.638l-.058.07-3.005 3.004a2.5 2.5 0 0 0 3.405 3.658l.13-.122 3.006-3.005a.5.5 0 0 1 .638-.058l.069.058.707.707a.5.5 0 0 1 .058.638l-.058.069-3.005 3.005a4.5 4.5 0 0 1-6.524-6.196l.16-.168 3.005-3.005zm8.132-3.182a.25.25 0 0 1 .353 0l1.061 1.06a.25.25 0 0 1 0 .354l-8.132 8.132a.25.25 0 0 1-.353 0l-1.061-1.06a.25.25 0 0 1 0-.354l8.132-8.132z"/></svg></span>docs.google.com</span></span><span class="LinkCard-imageCell">
<svg class="Zi Zi--Browser" fill="currentColor" viewbox="0 0 24 24" width="32" height="32"><path d="M11.991 3C7.023 3 3 7.032 3 12s4.023 9 8.991 9C16.968 21 21 16.968 21 12s-4.032-9-9.009-9zm6.237 5.4h-2.655a14.084 14.084 0 0 0-1.242-3.204A7.227 7.227 0 0 1 18.228 8.4zM12 4.836A12.678 12.678 0 0 1 13.719 8.4h-3.438A12.678 12.678 0 0 1 12 4.836zM5.034 13.8A7.418 7.418 0 0 1 4.8 12c0-.621.09-1.224.234-1.8h3.042A14.864 14.864 0 0 0 7.95 12c0 .612.054 1.206.126 1.8H5.034zm.738 1.8h2.655a14.084 14.084 0 0 0 1.242 3.204A7.188 7.188 0 0 1 5.772 15.6zm2.655-7.2H5.772a7.188 7.188 0 0 1 3.897-3.204c-.54.999-.954 2.079-1.242 3.204zM12 19.164a12.678 12.678 0 0 1-1.719-3.564h3.438A12.678 12.678 0 0 1 12 19.164zm2.106-5.364H9.894A13.242 13.242 0 0 1 9.75 12c0-.612.063-1.215.144-1.8h4.212c.081.585.144 1.188.144 1.8 0 .612-.063 1.206-.144 1.8zm.225 5.004c.54-.999.954-2.079 1.242-3.204h2.655a7.227 7.227 0 0 1-3.897 3.204zm1.593-5.004c.072-.594.126-1.188.126-1.8 0-.612-.054-1.206-.126-1.8h3.042c.144.576.234 1.179.234 1.8s-.09 1.224-.234 1.8h-3.042z"/></svg>
</span></span>

大家自己看这个设计文档,恐怕比较晦涩。没关系,这正是这个系列文章的会帮助大家的地方。

那么,欲知后事如何,请听下回分解~

<span class="LinkCard-backdrop" style="background-image:url(https://zhstatic.zhihu.com/assets/zhihu/editor/zhihu-card-default.svg)"/><span class="LinkCard-content"><span class="LinkCard-text"><span class="LinkCard-title" data-text="true">王益:Go的隐秘世界:Goroutine调度机制概览</span><span class="LinkCard-meta"><span style="display:inline-flex;align-items:center">​<svg class="Zi Zi--InsertLink" fill="currentColor" viewbox="0 0 24 24" width="17" height="17"><path d="M13.414 4.222a4.5 4.5 0 1 1 6.364 6.364l-3.005 3.005a.5.5 0 0 1-.707 0l-.707-.707a.5.5 0 0 1 0-.707l3.005-3.005a2.5 2.5 0 1 0-3.536-3.536l-3.005 3.005a.5.5 0 0 1-.707 0l-.707-.707a.5.5 0 0 1 0-.707l3.005-3.005zm-6.187 6.187a.5.5 0 0 1 .638-.058l.07.058.706.707a.5.5 0 0 1 .058.638l-.058.07-3.005 3.004a2.5 2.5 0 0 0 3.405 3.658l.13-.122 3.006-3.005a.5.5 0 0 1 .638-.058l.069.058.707.707a.5.5 0 0 1 .058.638l-.058.069-3.005 3.005a4.5 4.5 0 0 1-6.524-6.196l.16-.168 3.005-3.005zm8.132-3.182a.25.25 0 0 1 .353 0l1.061 1.06a.25.25 0 0 1 0 .354l-8.132 8.132a.25.25 0 0 1-.353 0l-1.061-1.06a.25.25 0 0 1 0-.354l8.132-8.132z"/></svg></span>zhuanlan.zhihu.com</span></span><span class="LinkCard-imageCell"></span></span>

到此这篇关于“ Go的隐秘世界:Go程序的启动和runtime初始化”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
Go的隐秘世界:一个Goroutine要几个Thread
Go的隐秘世界:从 Cgo 到 Goroutine 调度
Go的隐秘世界:Go程序的启动和runtime初始化
Go的隐秘世界:有Thread为啥还要Goroutine
Go的隐秘世界:Goroutine调度机制概览
Go 语言十年而立,Go2 蓄势待发
龙芯平台构建Go语言环境指南
Go 开发关键技术指南 | 为什么你要选择 Go?(内含超全知识大图)
也谈goroutine调度器
go 语言学习历程

[关闭]
~ ~