在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

Part1 引言

go語言的一個(gè)優(yōu)勢(shì)是能夠生成靜態(tài)鏈接的可執(zhí)行程序。但是,這并不是說默認(rèn)情況下編譯出來的Go可執(zhí)行程序都是靜態(tài)鏈接的。在有些情況下,需要額外的操作才能實(shí)現(xiàn)。具體情況取決于操作系統(tǒng),本文介紹unix系統(tǒng)下如何達(dá)成這一目標(biāo)。

Part2 示例

下面是用Go語言編寫的hello world程序,在linux機(jī)器上將其編譯成可執(zhí)行文件。然后檢查該可執(zhí)行文件是靜態(tài)鏈接還是動(dòng)態(tài)鏈接。

代碼語言:JavaScript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

package mainimport "fmt"func main() { fmt.Println("hello world")}

編譯helloworld可執(zhí)行文件,操作如下。

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]
在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

Part3 檢查可執(zhí)行程序是動(dòng)態(tài)鏈接還是靜態(tài)鏈接

有多種方法檢查可執(zhí)行程序鏈接類型,這里介紹三種方法:

1 通過file命令

file命令輸出中明確說明helloworld可執(zhí)行文件為 Statically linked類型,即靜態(tài)鏈接。

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

2 通過ldd命令

ldd命令會(huì)輸出helloworld程序依賴的動(dòng)態(tài)庫,由于helloworld非動(dòng)態(tài)鏈接,所以輸出結(jié)果為 不是動(dòng)態(tài)可執(zhí)行程序

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]3 通過nm命令

使用nm命令列舉出helloworld中未定義符號(hào)(期望在鏈接運(yùn)行時(shí)通過動(dòng)態(tài)庫加載)。輸出為空,表明helloworld中沒有任何未定義符號(hào)。

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

Part4 DNS和用戶組

在Unix機(jī)器上,當(dāng)滿足特定條件時(shí),Go語言標(biāo)準(zhǔn)庫會(huì)借助libc實(shí)現(xiàn)DNS和用戶組功能。

當(dāng)cgo啟用時(shí)(默認(rèn)情況下,在Unix系統(tǒng)下 CGO_ENABLED 值為1,表示默認(rèn)開啟)。Go語言標(biāo)準(zhǔn)庫中的net包會(huì)調(diào)用c語言庫實(shí)現(xiàn)DNS查找功能。這意味著Go程序在進(jìn)行域名解析時(shí),會(huì)使用底層操作系統(tǒng)提供的C庫函數(shù),如getaddrinfo和getnameinfo。同理,當(dāng)啟用cgo時(shí),Go語言標(biāo)準(zhǔn)庫中的os/user包會(huì)調(diào)用C語言庫來查找用戶和用戶組。

DNS查詢代碼如下,保存在lookuphost.go文件中。

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

package mainimport ( "fmt" "net")func main() { fmt.Println(net.LookupHost("go.dev"))}
在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

編譯出可執(zhí)行程序 lookuphost

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

通過ldd命令可以看出lookuphost可執(zhí)行程序是一個(gè)動(dòng)態(tài)鏈接,需要在運(yùn)行時(shí)通過ilbc加載共享庫。

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

為啥編譯出來的程序是動(dòng)態(tài)鏈接在官方net包文檔中有詳細(xì)說明。Go語言標(biāo)準(zhǔn)庫也提供了純Go實(shí)現(xiàn)的DNS功能(盡管純Go實(shí)現(xiàn)可能缺少一些高級(jí)特性),如果我們想使用純Go實(shí)現(xiàn),而不是依賴于系統(tǒng)的libc,可以在編譯的時(shí)候設(shè)置tags實(shí)現(xiàn)。具體操作如下:

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

此外,我們還可以通過關(guān)閉cgo來編譯出靜態(tài)鏈接程序。在Unix系統(tǒng)中,默認(rèn)cgo是開啟的,查看方法如下。

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

關(guān)閉cgo再次編譯lookuphost程序。

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

查找用戶的用戶組信息程序如下,代碼保存在userlookup.go文件中。

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

package mainimport (  "encoding/json"  "log"  "os"  "os/user")func main() {  user, err := user.Lookup("bob")  if err != nil {    log.Fatal(err)  }  je := json.NewEncoder(os.Stdout)  je.Encode(user)}

編譯上述代碼,然后通過ldd命令看到可執(zhí)行程序?yàn)閯?dòng)態(tài)鏈接。

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

同上面的DNS程序,我們可以添加編譯tags,將其編譯為靜態(tài)鏈接程序。

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

此外,我們也可以通過關(guān)閉cgo達(dá)到同樣的效果。

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

Part5 將C代碼鏈接到Go二進(jìn)制文件

Go語言支持通過cgo調(diào)用C語言中的函數(shù)接口(FFI),下面通過一個(gè)具體例子說明,下述代碼保存在cstdio.go文件中。

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

package main//#include <stdio.h>// void helloworld(){// printf("hello,world from Cn");//}import "C"func main() { C.helloworld()}

由于使用了cgo,C代碼中調(diào)用了printf函數(shù),該函數(shù)屬于libc,即使程序中沒有顯式調(diào)用C運(yùn)行時(shí),當(dāng)Go代碼通過cgo與C代碼交互時(shí),cgo會(huì)生成一些“膠水代碼”,確保程序能夠正常執(zhí)行。通過ldd命令可以看到上述程序編譯后的可執(zhí)行文件為動(dòng)態(tài)鏈接類型。

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

注意:即使我們編寫的程序中沒有C代碼,cgo也可能會(huì)涉及。因?yàn)槲覀兂绦蛞玫囊蕾噹炜赡苁褂昧薱go,像常用的go-sqlite3驅(qū)動(dòng)程序就需要cgo,如果我們編寫的程序?qū)肓嗽摪瑒t自然使用了cgo。

這種情況下,通過關(guān)閉CGO_ENABLED是無效的。那有什么解決方法嗎?請(qǐng)看下一章節(jié)內(nèi)容。

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

Part6 鏈接靜態(tài)libc

如果Go程序包含了C代碼,在Unix系統(tǒng)上編譯出來的二進(jìn)制文件是動(dòng)態(tài)鏈接。具體原因如下:

C代碼調(diào)用libc(C運(yùn)行時(shí))在Unix系統(tǒng)上通常使用的libc是glibc推薦的方式是通過動(dòng)態(tài)鏈接到glibc因此,go build得到的二進(jìn)制文件是動(dòng)態(tài)鏈接類型

我們可以換用其他的libc,而不是默認(rèn)的glibc,比如使用靜態(tài)鏈接的libc,這樣編譯后的可執(zhí)行文件就是靜態(tài)的。目前musl就是一個(gè)滿足我們期望的libc,它是一個(gè)輕量級(jí)的C標(biāo)準(zhǔn)庫,兼容POSIX的API,是許多靜態(tài)鏈接應(yīng)用程序和容器化應(yīng)用程序的首選。

1 安裝musl從官網(wǎng)下載musl源碼

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

解壓源碼壓縮包 musl-1.2.5.tar.gz

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

進(jìn)入 musl-1.2.5目錄,執(zhí)行 ./configure –prefix=, MUSLDIR是安裝路徑,我這里選擇的是/usr/local/bin/最后執(zhí)行 make / make install 進(jìn)行安裝, 安裝成功如下

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

2 編譯

下面采用musl對(duì)cstdio.go文件進(jìn)行重行編譯,操作如下。CC告訴go build 使用哪個(gè)c編譯器進(jìn)行cgo編譯。后面的連接器參數(shù)設(shè)置使用外部連接器。最后執(zhí)行靜態(tài)鏈接。詳細(xì)信息閱讀官方文檔

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

上述編譯靜態(tài)程序的方法同樣適用于復(fù)雜程序,作者提供了一個(gè)use-sqlite.go文件,使用了go-sqlite3包,下面通過兩種方式編譯對(duì)比效果。

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

// use-sqlite.go package mainimport ( "database/sql" "fmt" "log" _ "github.com/mattn/go-sqlite3")func main() { // Open the database file in /tmp/ db, err := sql.Open("sqlite3", "/tmp/example.db") if err != nil {  log.Fatal(err) } defer db.Close() // Create a table if it doesn't exist createTableSQL := `CREATE TABLE IF NOT EXISTS users (  id INTEGER PRIMARY KEY AUTOINCREMENT,  name TEXT NOT NULL,  age INTEGER NOT NULL );` _, err = db.Exec(createTableSQL) if err != nil {  log.Fatal(err) } // Insert some data insertUserSQL := `INSERT INTO users (name, age) VALUES (?, ?)` _, err = db.Exec(insertUserSQL, "Alice", 30) if err != nil {  log.Fatal(err) } _, err = db.Exec(insertUserSQL, "Bob", 25) if err != nil {  log.Fatal(err) } // Query the data querySQL := `SELECT id, name, age FROM users` rows, err := db.Query(querySQL) if err != nil {  log.Fatal(err) } defer rows.Close() fmt.Println("User data:") for rows.Next() {  var id int  var name string  var age int  err = rows.Scan(&id, &name, &age)  if err != nil {   log.Fatal(err)  }  fmt.Printf("ID: %d, Name: %s, Age: %dn", id, name, age) } err = rows.Err() if err != nil {  log.Fatal(err) }}

采用默認(rèn)編譯器編譯

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

采用musl編譯

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

采用musl,我們可以在不使用-tags netgo標(biāo)簽,也不用禁用cgo的情況,編譯一個(gè)靜態(tài)鏈接的lookuphost程序。

在Linux環(huán)境中使用Go編譯靜態(tài)二進(jìn)制文件[譯]

Part7 使用Zig作為C編譯器

Zig 是一種新的系統(tǒng)編程語言,與Go語言類似,它也提供了一系列編譯工具即Zig工具鏈,該工具鏈包含有Zig編譯器、C/c++編譯器、鏈接器和用于靜態(tài)鏈接的libc。所以Zig可以用來將Go二進(jìn)制文件與C代碼靜態(tài)鏈接。

安裝Zig后采用下面的命令編譯可執(zhí)行程序,其中ZIGDIR為Zig的安裝目錄。相比上一章節(jié)的musl-gcc,調(diào)用命令會(huì)簡(jiǎn)單一些。

代碼語言:javascript代碼運(yùn)行次數(shù):0運(yùn)行復(fù)制

$ CC="$ZIGDIR/zig cc -target x86_64-linux-musl" go build cstdio.go$ CC="$ZIGDIR/zig cc -target x86_64-linux-musl" go build use-sqlite.go

Part8 總結(jié)大部分情況下Go語言編譯出來的可執(zhí)行程序是靜態(tài),但在某些情況下默認(rèn)編譯出來的是動(dòng)態(tài)鏈接,我們可以設(shè)置build tags或關(guān)閉cgo來編譯靜態(tài)鏈接程序。Go語言中有一個(gè)提案,即在 go build 命令中添加一個(gè) -static 標(biāo)志,表明將程序編譯為靜態(tài)類型,目前還未實(shí)現(xiàn)。

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點(diǎn)贊6 分享