musl libc是C語言的一種標準函式庫,程式碼乾淨且高效,針對靜態連接(static linking)設計,適合被用來製作可攜的程式,且也很容易進行交叉編譯(cross compile),編譯出運行在不同系統環境的程式。GCC(GNU Compiler Collection)是GNU的C/C++編譯器套裝,大部分的Linux發行版使用的C標準函式庫是glibc,其所提供的GCC預設也是基於glibc,雖然glibc效能挺好,但同樣的已編譯好的函式庫和執行檔在不同的Linux上可能無法共用,要分別編譯或者用某種方式打包起來才行。不過如果是使用基於musl libc的GCC,就可以一次編譯出可以在相同CPU架構的Linux發行版上都能運行的程式。
glibc 會有的問題
在使用musl libc之前,先來看看glibc到底會有什麼樣的問題。
以在Linux Mint 20.3上使用預設的GCC編譯OpenSSL來說明,可以先從OpenSSL的GitHub倉庫上取得OpenSSL的原始碼專案。在此以1_1_1q
版的OpenSSL為例。
執行以下指令,會使用預設的GCC來編譯OpenSSL,並安裝到原始碼專案下的output
目錄中。此種編譯方式會產生共享函式庫(shared library)並進行動態連結(dynamic linking)。
./Configure linux-x86_64 --prefix="$(pwd)/output" && make clean && make -j$(nproc) && make install
接著可以使用ldd
指令查看openssl
執行檔有動態連結到哪些共享函式庫。
如上圖,可以看到openssl
有動態連結到libssl.so
和libc.so
等函式庫,此時的openssl
檔案大小僅856KB。由於libssl.so
等OpenSSL所編譯出來的共享函式庫並未安裝在系統環境中,所以在執行openssl
時要使用LD_LIBRARY_PATH
變數來指定這些共享函式庫的所在目錄才能成功執行。
用這樣的方式編譯出來的openssl
執行檔,若要拿到別的x86_64的Linux作業系統上使用,該系統上必須要安裝OpenSSL所編譯出來的共享函式庫。不然的話就要把這些共享函式庫跟openssl
執行檔一起拿到別的Linux作業系統上使用才行。
即便如此,如果別的Linux作業系統使用的glibc版本和編譯程式的Linux作業系統使用的glibc版本不相容的話,openssl
執行檔也還是不能被成功執行的。下面就來做個實驗試試。
執行以下指令(多了no-shared
參數),會使用預設的GCC來編譯OpenSSL。此種編譯方式只會產生靜態函式庫(static library)並進行靜態連結與動態連結。
./Configure no-shared linux-x86_64 --prefix="$(pwd)/output" && make clean && make -j$(nproc) && make install
接著可以使用ldd
指令查看openssl
執行檔有動態連結到哪些共享函式庫。
如上圖,可以看到由於沒有編譯OpenSSL的共享函式庫,原先的libssl.so
等函式庫所擁有的那些功能,被直接放進了openssl
執行檔中,使得檔案大小也上升到了4.2MB,此時不需要使用LD_LIBRARY_PATH
變數也可以執行openssl
執行檔。不過這個openssl
執行檔也還是依賴於glibc,拿到如CentOS 7這種glibc版本比較舊的Linux發行版上執行,就會執行失敗。
至於要如何查看glibc的版本,可以執行以下指令:
難道我們就不能不去動態連結glibc嗎?其實可以,GCC可以加上-static
或是--static
參數(兩者的功用是一樣的)來禁用動態連結功能。執行以下指令,會使用預設的GCC來編譯OpenSSL。此種編譯方式只會產生靜態函式庫(static library)並進行靜態連結。
export CC="gcc -static"
./Configure no-shared linux-x86_64 --prefix="$(pwd)/output" && make clean && make -j$(nproc) && make install
您可能會看到編譯過程中出現了一些警告或是錯誤。如果最後還是成功編譯並安裝,可以使用ldd
指令查看openssl
執行檔有動態連結到哪些共享函式庫。
如上圖,openssl
執行檔已經不會去做動態連結了。但它的檔案大小增加到了5.1MB,且執行失敗……
嘗試去靜態連結glibc,不是一個很安全的作法。有時會出現問題。
編譯基於 musl libc 的 GCC
編譯腳本的GitHub倉庫:
要先安裝編譯時需要的套件。
基於Debian的Linux發行版可以執行以下指令:
紅帽系的Linux發行版可以執行以下指令:
執行以下指令取得編譯腳本:
在musl-cross-make
目錄中執行以下指令建立編譯設定檔:
在config.mak
檔案中加入以下設定:
TARGET = x86_64-linux-musl GCC_VER = 11.2.0 COMMON_CONFIG += CFLAGS="-g0 -O3" CXXFLAGS="-g0 -O3" LDFLAGS="-s" GCC_CONFIG += --enable-default-pie --enable-static-pie
TARGET
用來設定編譯出來的GCC是要用來編譯哪個平台的程式。如果您需要交叉編譯,要自行修改這個設定值。
GCC_VER
變數用來指定GCC的版本,雖然腳本都會自動從網際網路上下載指定版本的程式原始碼來編譯,但必須只有SHA1校驗碼有被放在hashes
目錄的程式原始碼才能被成功下載。
COMMON_CONFIG
用來設定編譯選項。-g0
是要關閉偵錯資訊;-O3
可進行運算速度優先的優化。-s
可以用來禁用符號表(symbol table),縮減執行檔的檔案大小。
GCC_CONFIG
用來設定GCC的功能。--enable-default-pie
表示預設要將程式編譯成PIC(position-independent code)和PIE(position-independent executable)。要編譯出動態函式庫,就會需要PIC;要編譯出動態連結的執行檔,就會需要PIE。啟用了--enable-default-pie
就不用在編譯的時候再下-fPIC
-fPIE
等參數了,大部分的Linux發行版所提供的GCC都有啟用這個選項。--enable-static-pie
可以啟用靜態PIE的功能。
執行以下指令開始編譯,並將結果安裝到musl-cross-make
目錄中的output
目錄:
將output
目錄下的檔案複製到更容易記憶的地方。例如筆者習慣將其複製到/opt/musl
目錄下,所以執行以下指令:
至此就不需要再使用musl-cross-make
目錄了,可以砍掉。
執行以下指令快速修改/opt/musl
目錄下,函式庫檔案的所在路徑。這個路徑是要給libtool看的。
sudo find /opt/musl -iname "*.la" -type f -exec sed -i "s/libdir='/libdir='\/opt\/musl/g" "{}" \;
如果您編譯出來的程式不是只運行在基於musl libc的Linux發行版,那麼可以考慮再執行以下指令,對/opt/musl
目錄下的.la
檔案再進行調整。
sudo find /opt/musl -iname "*.la" -type f -exec sed -i "s/installed=yes/installed=no/g" "{}" \;
以上指令會將.la
檔案的installed=yes
改為installed=no
。這樣做的原因是,libtool的連結模式(--mode=link
)下使用-static
參數時,如果函式庫已經被安裝在系統,就會嘗試去連結其共享函式庫,而不是靜態函式庫。當CC
或是CXX
環境變數設成如musl-gcc -static --static
(-static
參數是預期給libtool --mode=link
吃的參數;--static
才是預期給musl-gcc
吃的參數)時,若installed=yes
(函式庫被安裝在系統了),則libtool可能會查找出共享函式庫的路徑,導致連結失敗,因為此時的GCC的動態連結是被禁用的。
用 x86_64-linux-musl-gcc 編譯程式試試
再次於同一個環境上編譯OpenSSL,只不過這回要使用musl libc。
執行以下指令,會使用基於musl libc的GCC來編譯OpenSSL,並安裝到原始碼專案下的output
目錄中。此種編譯方式只會產生靜態函式庫(static library)並進行靜態連結。
export CC="/opt/musl/bin/x86_64-linux-musl-gcc -static"
./Configure no-shared linux-x86_64 --prefix="$(pwd)/output" && make clean && make -j$(nproc) && make install
接著可以使用ldd
指令查看openssl
執行檔有動態連結到哪些共享函式庫。
如上圖,openssl
執行檔沒有動態連結,檔案大小是合理的4.2MB,且可以正常執行。
把這個openssl
執行檔單獨拿到CentOS 7上也依然可以正常執行,如下圖: