golang 快速入門 [5.1]-go 語言是如何執行的-連結器

weishixianglian發表於2020-03-17

golang 快速入門 [5.1]-go 語言是如何執行的-連結器

前文

前言

  • 在上一篇文章中,我們詳細介紹了 go 語言編譯為機器碼經歷的:詞法分析 => 語法分析 => 型別檢查 => 中間程式碼 => 程式碼優化 => 生成機器碼
  • 但是在原始碼生成執行程式的過程中,其實還經歷了連結等過程。總的來說一個程式的生命週期可以概括為: 編寫程式碼 => 編譯 => 連結 => 載入到記憶體 => 執行
  • 在第 5 章我們將對其進行逐一解釋

連結 (link)

  • 我們編寫的程式可能會使用其他程式或程式庫 ( library ) 正如我們在 helloworld 程式中使用的 fmt package
  • 我們編寫的程式必須與這些程式或程式庫一起才能夠執行
  • 連結是將我們編寫的程式與我們需要的外部程式組合在一起的過程
  • 連結器是系統軟體,在系統開發中起著至關重要的作用,因為它可以進行單獨的編譯。您可以將它分解為更小,更易管理的塊,然後分別進行修改和編譯,而不是將一個大型應用程式組織為一個整體的原始檔。當您更改其中一個模組時,只需重新編譯它並重新連結應用程式,而無需重新編譯其他原始檔。
  • 連結分為兩種,靜態連結與動態連結
  • 靜態連結的特點在於連結器會將程式中使用的所有庫程式複製到最後的可執行檔案中。而動態連結只會在最後的可執行檔案中儲存動態連結庫的位置,並在執行時呼叫。
  • 因此靜態連結要更快,可移植,因為它不需要在執行它的系統上存在該庫。但是在磁碟和記憶體上佔用更多的空間
  • 連結發生的過程會在兩個地方,一種是靜態連結會在編譯時的最後一步發生,一種是動態連結在程式載入到記憶體時發生。

  • 下面我們簡單對比一下靜態連結與動態連結

靜態連結 動態連結
靜態連結是將程式中使用的所有庫模組複製到最終可執行檔案的過程,這是由連結器執行的,並且是編譯過程的最後一步。 載入程式後,作業系統會將包含可執行程式碼和資料的單個檔案放入記憶體,該靜態連結檔案包括呼叫程式和被呼叫程式. 在動態連結中,外部庫(共享庫)的地址放置在最終的可執行檔案中,而實際連結是在執行時將可執行檔案和庫都放置在記憶體中時進行的,動態連結使多個程式可以使用可執行模組的單個副本。
靜態連結由稱為連結器的程式執行,是編譯程式的最後一步 動態連結由作業系統在執行時執行
靜態連結檔案的大小明顯更大,因為外部程式內建在可執行檔案中 在動態連結中,共享庫中只有一個副本保留在記憶體中。 這大大減小了可執行程式的大小,從而節省了記憶體和磁碟空間
在靜態連結中,如果任何外部程式已更改,則必須重新編譯並重新連結它們,否則更改將不會反映在現有的可執行檔案中 在動態連結中不同,只需要更新和重新編譯各個共享模組程式即可變動。這是動態連結所提供的最大優勢之一
靜態連結的程式每次將其載入到記憶體中執行時,都會花費恆定的載入時間 動態連結中,如果共享庫程式碼已存在於記憶體中,則可以減少載入時間
使用靜態連結庫的程式通常比使用共享庫的程式快 使用共享庫的程式通常比使用靜態連結庫的程式要慢。
在靜態連結程式中,所有程式碼都包含在一個可執行模組中。 因此,它們永遠不會遇到相容性問題。 動態連結的程式依賴於具有相容的庫。 如果更改了庫(例如,新的編譯器版本可能更改了庫),則可能必須重新設計應用程式以使其與該庫的新版本相容。 如果從系統中刪除了一個庫,則使用該庫的程式將不再起作用。

go 語言是靜態連結還是動態連結?

  • 有時會看到一些比較老的文章說 go 語言是靜態連結的,但這種說法是不準確的
  • 現在的 go 語言不僅支援靜態連結也支援動態編譯
  • 總的來說,go 語言在一般預設情況下是靜態連結的,但是一些特殊的情況,例如使用了 CGO(即引用了 C 程式碼)的地方,則會使用作業系統的動態連結庫。例如 go 語言的net/http包在預設情況下會應用libpthreadlibc 的動態連結庫,這種情況會導致 go 語言程式虛擬記憶體的增加(下一文介紹)
  • go 語言也支援在go build編譯時傳遞引數來指定要生成的連結庫的方式,我們可以使用go help build命令檢視

    » go help buildmode                                                                                                                                                             jackson@192
    -buildmode=archive
        Build the listed non-main packages into .a files. Packages named
        main are ignored.
    
    -buildmode=c-archive
        Build the listed main package, plus all packages it imports,
        into a C archive file. The only callable symbols will be those
        functions exported using a cgo //export comment. Requires
        exactly one main package to be listed.
    
    -buildmode=c-shared
        Build the listed main package, plus all packages it imports,
        into a C shared library. The only callable symbols will
        be those functions exported using a cgo //export comment.
        Requires exactly one main package to be listed.
    
    -buildmode=default
        Listed main packages are built into executables and listed
        non-main packages are built into .a files (the default
        behavior).
    
    -buildmode=shared
        Combine all the listed non-main packages into a single shared
        library that will be used when building with the -linkshared
        option. Packages named main are ignored.
    
    -buildmode=exe
        Build the listed main packages and everything they import into
        executables. Packages not named main are ignored.
    
    -buildmode=pie
        Build the listed main packages and everything they import into
        position independent executables (PIE). Packages not named
        main are ignored.
    
    -buildmode=plugin
        Build the listed main packages, plus all packages that they
        import, into a Go plugin. Packages not named main are ignored.
    
  • archive: 將非 main package 構建為 .a 檔案. main 包將被忽略。

  • c-archive: 將 main package 構建為及其匯入的所有 package 構建為構建到 C 歸檔檔案中

  • c-shared: 將 mainpackage 構建為,以及它們匯入的所有 package 構建到 C 動態庫中。

  • shared: 將所有非 main package 合併到一個動態庫中,當使用-linkshared 引數後,能夠使用此動態庫

  • exe: 將 main package 和其匯入的 package 構建為成為可執行檔案

  • 本文不再介紹 go 如何手動使用動態庫這一高階功能,讀者只需現在知道 go 可以實現這一功能即可

編譯與連結的具體過程

  • 下面我們以 helloworld 程式為例,來說明 go 語言編譯與連結的過程,我們可以使用go build命令,-x引數代表了列印執行的過程 go build -x main.go 輸出如下: WORK=/var/folders/g2/0l4g444904vbn8wxnrw0j_980000gn/T/go-build757876739 mkdir -p $WORK/b001/ cat >$WORK/b001/importcfg << 'EOF' # internal # import config packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a EOF cd /Users/jackson/go/src/viper/XXX /usr/local/go/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -complete -buildid JqleDuJlC1iLMVADicsQ/JqleDuJlC1iLMVADicsQ -goversion go1.13.6 -D _/Users/jackson/go/src/viper/args -importcfg $WORK/b001/importcfg -pack -c=4 ./main.go /usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal cp $WORK/b001/_pkg_.a /Users/jackson/Library/Caches/go-build/cf/cf0dc65f39f01c8494192fa8af14570b445f6a25b762edf0b7258c22d6e10dc8-d # internal cat >$WORK/b001/importcfg.link << 'EOF' # internal packagefile command-line-arguments=$WORK/b001/_pkg_.a packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a packagefile errors=/usr/local/go/pkg/darwin_amd64/errors.a ... EOF mkdir -p $WORK/b001/exe/ cd . /usr/local/go/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=zCU3mCFNeUDzrRM33f4L/JqleDuJlC1iLMVADicsQ/r7xJ7p5GD5T9VONtmxob/zCU3mCFNeUDzrRM33f4L -extld=clang $WORK/b001/_pkg_.a /usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal mv $WORK/b001/exe/a.out main rm -r $WORK/b001/
  • 下面我們對輸出進行逐行分析
  • 建立了一個臨時目錄,用於存放臨時檔案。預設情況下命令結束時自動刪除此目錄,如果需要保留新增-work引數。 WORK=/var/folders/g2/0l4g444904vbn8wxnrw0j_980000gn/T/go-build757876739 mkdir -p $WORK/b001/ cat >$WORK/b001/importcfg << 'EOF' # internal
  • 生成編譯配置檔案,主要為編譯過程需要的外部依賴(如:引用的其他包的函式定義) # import config packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a
  • 編譯,生成中間結果$WORK/b001/_pkg_.a,
/usr/local/go/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -complete -buildid JqleDuJlC1iLMVADicsQ/JqleDuJlC1iLMVADicsQ -goversion go1.13.6 -D _/Users/jackson/go/src/viper/args -importcfg $WORK/b001/importcfg -pack -c=4 ./main.go
  • .a 檔案由 compile 命令生成,也可以通過go tool compile進行呼叫
  • .a 型別的檔案又叫做目標檔案 (object file),其是一個壓縮包,內部包含了__.PKGDEF_go_.o 兩個檔案,分別為編譯目標檔案和連結目標檔案

    $ file _pkg_.a # 檢查檔案格式
    _pkg_.a: current ar archive # 說明是ar格式的打包檔案
    $ ar x _pkg_.a #解包檔案
    $ ls
    __.PKGDEF  _go_.o
    
  • 檔案內容由程式碼匯出的函式、變數以及引用的其他包的資訊組成。為了弄清這兩個檔案包含的資訊需要檢視 go 編譯器實現的相關程式碼,相關程式碼在src/cmd/compile/internal/gc/obj.go檔案中(原始碼中的檔案內容可能隨版本更新變化,本系列文章以 Go1.13.5 版本為準)

  • 下面程式碼中生成 ar 檔案,ar 檔案 是一種非常簡單的打包檔案格式,廣泛用於 linux 中靜態連結庫檔案中,檔案以 字串"!<arch>\n"開頭。隨後跟著 60 位元組的檔案頭部(包含檔名、修改時間等資訊),之後跟著檔案內容。因為 ar 檔案格式簡單,Go 編譯器直接在函式中實現了 ar 打包過程。

  • startArchiveEntry 用於預留 ar 檔案頭資訊位置(60 位元組),finishArchiveEntry 用於寫入檔案頭資訊,因為檔案頭資訊中包含檔案大小,在寫入完成之前檔案大小未知,所以分兩步完成。

func dumpobj1(outfile string, mode int) {
    bout, err := bio.Create(outfile)
    if err != nil {
        flusherrors()
        fmt.Printf("can't create %s: %v\n", outfile, err)
        errorexit()
    }
    defer bout.Close()
    bout.WriteString("!<arch>\n")

    if mode&modeCompilerObj != 0 {
        start := startArchiveEntry(bout)
        dumpCompilerObj(bout)
        finishArchiveEntry(bout, start, "__.PKGDEF")
    }
    if mode&modeLinkerObj != 0 {
        start := startArchiveEntry(bout)
        dumpLinkerObj(bout)
        finishArchiveEntry(bout, start, "_go_.o")
    }
}

  • 生成連結配置檔案,主要為需要連結的其他依賴
cat >$WORK/b001/importcfg.link << 'EOF' # internal
packagefile command-line-arguments=$WORK/b001/_pkg_.a
packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a
packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a
packagefile errors=/usr/local/go/pkg/darwin_amd64/errors.a
...
EOF
  • 執行連結器,生成最終可執行檔案main,同時可執行檔案會拷貝到當前路徑,最後刪除臨時檔案
/usr/local/go/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=zCU3mCFNeUDzrRM33f4L/JqleDuJlC1iLMVADicsQ/r7xJ7p5GD5T9VONtmxob/zCU3mCFNeUDzrRM33f4L -extld=clang $WORK/b001/_pkg_.a
/usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out main
rm -r $WORK/b001/

總結

  • 在本文中,我們介紹了 go 程式從原始碼到執行需要經歷的重要一環——連結,並介紹了靜態連結與動態連結
  • 在本文中,我們用一個例子介紹了編譯與連結的具體過程
  • 在下文中,我們將介紹 go 語言的記憶體分配

參考資料

喜歡本文的朋友歡迎點贊分享~

唯識相鏈啟用微信交流群(Go 與區塊鏈技術) 歡迎加微信:ywj2271840211

更多原創文章乾貨分享,請關注公眾號
  • golang 快速入門 [5.1]-go 語言是如何執行的-連結器
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章