您當前的位置:首頁 > 舞蹈

Go的隱秘世界:Go程式的啟動和runtime初始化

作者:由 王益 發表于 舞蹈時間:2020-09-14

書接上文: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 程式吧。

package

main

import

“fmt”

func

main

()

{

fmt

Println

“hello world”

}

首先我們編譯這個程式 a。go 到可執行檔案 a:

go build -o a a。go

我們在 Linux 下執行上述命令,得到的可執行檔案 a 是 Linux 的 ELF 可執行檔案格式。用 Linux 裡的 readelf 命令,我們可以列印 ELF 的檔案頭,其中有執行這個檔案時第一條指令所在的位置,也就是 a 的入口地址(entrypoint)。

root@7f2187b3c225:/go# readelf -h /tmp/a | grep -i entry

Entry point address: 0x4645e0

接下來,我們要看看這個入口地址 0x4645e0 指向的

彙編程式

。為此,我們用 objdump 命令反彙編 a,得到 a。S

objdump

-

S

a

>

a。S

用文字編輯器開啟 a。S,然後搜尋入口地址”4645e0“,我們找到以下程式碼。

00000000004645

e0

<

_rt0_amd64_linux

>

//

Copyright

2009

The

Go

Authors。

Al

l

rights

reserved。

4645

e0:

e9

1b

cb

ff

ff

jmpq

461100

<

_rt0_amd64

>

4645

e5:

cc

int3

可以看出來,這個入口是一個函式,叫做 _rt0_amd64_linux。根據下面的註釋,可以看到這個入口函式是 Go 編譯器生成的。它只有一行指令,跳轉到一個叫 _rt0_amd64 的函式。這個函式定義位於 Go runtime 裡,原始碼在 golang/go 。

TEXT _rt0_amd64(SB),NOSPLIT,$-8

MOVQ 0(SP), DI // argc

LEAQ 8(SP), SI // argv

JMP runtime·rt0_go(SB)

因為我是在 AMD64 系統上用 Linux 做上述實驗的,所以這個函式所在的檔名是 runtime/asm_amd64。s 。同一個目錄下,有其他彙編原始碼檔案,分別對應其他 CPU 體系結構,包括 ARM、PowerPC、MIPS 和 WASM(Web assembly)。

上述函式很簡單,只是呼叫了 Go runtime 裡另一個函式

rt0_go

。這個函式也在同一個彙編原始碼檔案裡。這個彙編函式定義略長,我們貼一個 GitHub permalink:

從這個函式的原始碼,大家可以看到 Go 原始碼庫裡的彙編程式是用的 Plan 9 彙編器的語法寫的。這個語法為了相容各種 CPU 體系結構,有一定的抽象,所以並不一定每一條

彙編指令

都一一對應到 CPU 指令。不過這些不妨礙我們閱讀程式碼,實際上簡化了程式碼閱讀。另外,雖然 Go 使用的彙編語法是 Plan 9 的,但是彙編器是自己實現的,並沒有複用 Plan 9 的彙編器。更多關於 Go 的

組合語言

的細節,可以看

https://

golang。org/doc/asm

。不過目前我們並不需要追溯這些細節。只需要注意,一個彙編函式以 TEXT directive 開頭,以 RET 指令(或者其他一條跳轉指令)結束。

這個 rt0_go 函式具體做了以下幾件事情:

呼叫 x_cgo_init 函式。

呼叫 runtime。osinit 函式。

呼叫 schedinit 函式。

建立 run queue 和一個新的 G(goroutine)。

呼叫 runtime·mstart 函式。

在接下來的文章裡,我們要深入分析 rt0_go 這個函式到底做了什麼。不過,為了大家看 runtime 程式碼看的明白,我們先得說說程式碼的設計思想,尤其是 Go runtime 的主要內容 Go scheduler 的設計思想,否則至少上面 4。 裡提到的

run queue

是啥 —— 讀者就懵了。那我又是怎麼知道這些設計思想的呢?我看了設計文件

大家自己看這個設計文件,恐怕比較晦澀。沒關係,這正是這個系列文章的會幫助大家的地方。

那麼,欲知後事如何,請聽下回分解~

標簽: go  runtime  函式  rt0