Golang 編譯:靜態鏈接和動態鏈接

本文介紹了 Go 語言中靜態鏈接和動態鏈接的概念,解釋了它們的區別和各自優勢。通過示例,展示瞭如何生成靜態或動態鏈接的二進制文件,以及使用工具進行檢查。文章還討論了內部和外部鏈接器的區別,如何在編譯時選擇鏈接方式,以及在交叉編譯時處理 cgo 的方法。最後,提到了減小二進制文件大小的技巧和安全性方面的考慮。

概述

Go 語言最大的優勢之一就是它的編譯器,它爲程序員抽象了許多細節,讓你可以輕鬆地爲幾乎任何平臺和架構 https://pkg.go.dev/cmd/dist 編譯你的程序。

儘管這看起來很簡單,但其中有一些細微的差別,同一個程序有多種編譯方式,這會導致生成不同的可執行文件。

在本文中,我們將探討靜態鏈接和動態鏈接的可執行文件、內部和外部鏈接器,並使用 fileldldd 等工具檢查二進制文件。

什麼是靜態鏈接和動態鏈接?

靜態鏈接是將程序所需的所有庫直接複製到最終可執行文件中的做法。

Go 語言非常喜歡並希望在可能的情況下這樣做,因爲這樣生成的二進制文件更加便攜,不需要在運行的主機系統上存在庫。因此,你的二進制文件可以在任何系統上運行,無論是哪個發行版或版本,而且不依賴任何系統庫。

另一方面,動態鏈接是在運行時按名稱將外部或共享庫加載到可執行文件中。

動態鏈接也有其自身的優勢。例如,程序可以重用主機系統上可用的常用 libc 庫,而不需要重新實現它們,你還可以在不重新鏈接程序的情況下受益於主機的更新。在許多情況下,它還可以減小可執行文件的大小。

靜態鏈接的程序

讓我們來看一個始終進行靜態鏈接的程序。這個程序沒有使用 cgo 調用 C 代碼,因此所有內容都可以打包在一個靜態二進制文件中。

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

什麼是二進制文件?

我們可以使用 file 命令首先檢查文件類型。

它告訴我們這是一個 ELF(可執行和可鏈接格式)可執行文件,並且是 “靜態鏈接” 的。

我們不會深入討論 ELF 是什麼,但需要知道的是還有其他可執行文件格式。ELF 是 Linux 上的默認格式,Mach-O 是 macOS 的默認格式,PE/PE32+ 是 Windows 的默認格式,等等。

注意:在本文中,我們將使用 Linux(Ubuntu)及其工具,然而在其他平臺上也可以進行類似的操作。

還有一個 Linux 程序 ldd,可以告訴我們二進制文件是靜態鏈接還是動態鏈接的。

$ ldd main
    not a dynamic executable

動態鏈接的程序

如前所述,Go 有一個機制叫 cgo,可以從 Go 調用 C 代碼,甚至 Go 的標準庫在多個地方使用了它。例如,在 net 包中,它使用標準的 C 庫來處理 DNS。

默認情況下,導入此類包或在代碼中使用 cgo 會生成一個動態鏈接的二進制文件,鏈接到那些 libc 庫。

我們可以再次使用 fileldd 程序來檢查第二個二進制文件。

file 命令現在顯示這是一個動態鏈接的二進制文件,而 ldd 則顯示了二進制文件的動態依賴關係。在這種情況下,它依賴於 libc.so.6ld-linux,後者是 Linux 系統的動態鏈接器。

我們可以讓它靜態鏈接嗎?

當你希望二進制文件靜態鏈接時,可能有多種原因,但主要原因是爲了簡化部署和分發。然而,並不總是有必要這樣做。通過鏈接 libc,你可以從主機更新中受益,並且在使用 net 包的情況下,可以利用 libc 中包含的複雜的 DNS 查找函數。

有趣的是,Go 的 net 包也有一個純 Go 版本,這使得在編譯時禁用 cgo 成爲可能。你可以通過指定構建標籤或使用 CGO_ENABLED=0 完全禁用 cgo 來實現。

上面的截圖證明在這兩種情況下,我們最終都得到了一個靜態二進制文件。

內部鏈接器 vs 外部鏈接器

鏈接器是一個程序,它讀取 main 包的 Go 存檔或對象,以及它的依賴項,並將它們組合成一個可執行的二進制文件。

默認情況下,Go 的工具鏈使用其內部鏈接器(go tool link),但是你可以在編譯時指定使用哪個鏈接器,這樣可以讓我們在獲得靜態二進制文件的同時,享受完整的 libc 功能。

在 Linux 上,默認的外部鏈接器是 gcc 的 ld。我們可以告訴它生成一個靜態二進制文件。

它能工作,但我們會收到一個警告。在我們的例子中,glibc 使用 libnss 來支持多種地址解析服務提供者,而你無法靜態鏈接 libnss。

其他使用 cgo 的包可能會產生類似的警告,你需要查看文檔來判斷它們是否嚴重。

交叉編譯

如介紹中所述,交叉編譯是 Go 的一個非常好的特性,它允許你爲幾乎任何平臺 / 架構編譯程序。然而,如果你的程序使用了 cgo,這可能會非常棘手,因爲交叉編譯 C 代碼通常很困難。

你可以通過爲目標操作系統和 / 或架構安裝工具鏈來解決這個問題。

如果可能,最好在交叉編譯時不要使用 cgo。你將得到靜態鏈接的穩定二進制文件。

加分項:減小二進制文件大小

你可能注意到,上面 file 命令的輸出包含:“not stripped”。這意味着我們的二進制文件中包含調試信息。但我們通常不需要它,刪除它可以減小二進制文件的大小。

這將刪除調試信息和符號表,減小二進制文件的大小。

當心:LD_PRELOAD 技巧

Linux 系統程序 ld-linux.so(動態鏈接器 / 加載器)使用 LD_PRELOAD 來加載指定的共享庫。特別是,在加載任何其他庫之前,動態加載器將首先加載 LD_PRELOAD 中的共享庫。

LD_PRELOAD 技巧是在動態鏈接的二進制文件中使用的一種強大技術,用於覆蓋或攔截對共享庫的函數調用。

通過將 LD_PRELOAD 環境變量設置爲指向自定義的共享對象文件,用戶可以將自己的代碼注入程序的執行中,從而有效地替換或增強現有的庫函數。

這種方法允許各種應用,例如調試、測試,甚至在不修改原始源代碼的情況下改變程序行爲。

這也表明靜態鏈接的二進制文件更安全,因爲它們不存在這個問題,因爲它們不依賴任何外部庫。此外,還有一個 “安全執行模式”——這是由 Linux 系統上的動態鏈接器實現的安全特性,用於在運行需要提升權限的程序時限制某些行爲。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/zTZUacFCH8SjLsQKzOPKyw