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執行檔有動態連結到哪些共享函式庫。

musl-libc

如上圖,可以看到openssl有動態連結到libssl.solibc.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執行檔有動態連結到哪些共享函式庫。

musl-libc

如上圖,可以看到由於沒有編譯OpenSSL的共享函式庫,原先的libssl.so等函式庫所擁有的那些功能,被直接放進了openssl執行檔中,使得檔案大小也上升到了4.2MB,此時不需要使用LD_LIBRARY_PATH變數也可以執行openssl執行檔。不過這個openssl執行檔也還是依賴於glibc,拿到如CentOS 7這種glibc版本比較舊的Linux發行版上執行,就會執行失敗。

musl-libc

至於要如何查看glibc的版本,可以執行以下指令:

ldd --version

musl-libc

musl-libc

難道我們就不能不去動態連結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執行檔有動態連結到哪些共享函式庫。

musl-libc

如上圖,openssl執行檔已經不會去做動態連結了。但它的檔案大小增加到了5.1MB,且執行失敗……

嘗試去靜態連結glibc,不是一個很安全的作法。有時會出現問題。

編譯基於 musl libc 的 GCC

編譯腳本的GitHub倉庫:

要先安裝編譯時需要的套件。

基於Debian的Linux發行版可以執行以下指令:

sudo apt install git build-essential

紅帽系的Linux發行版可以執行以下指令:

sudo dnf group info "Development Tools"

執行以下指令取得編譯腳本:

git clone --depth 1 https://github.com/richfelker/musl-cross-make.git

musl-libc

musl-cross-make目錄中執行以下指令建立編譯設定檔:

cp config.mak.dist config.mak

musl-libc

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

musl-libc

musl-libc

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目錄:

make -j$(nproc) && make install

musl-libc

output目錄下的檔案複製到更容易記憶的地方。例如筆者習慣將其複製到/opt/musl目錄下,所以執行以下指令:

sudo cp -r output /opt/musl

musl-libc

至此就不需要再使用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

如果您編譯出來的程式不是只運行在基於musl libc的Linux發行版,那麼可以考慮再執行以下指令,對/opt/musl目錄下的.la檔案再進行調整。

sudo find /opt/musl -iname "*.la" -type f -exec sed -i "s/installed=yes/installed=no/g" "{}" \;

musl-libc

以上指令會將.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執行檔有動態連結到哪些共享函式庫。

musl-libc

如上圖,openssl執行檔沒有動態連結,檔案大小是合理的4.2MB,且可以正常執行。

把這個openssl執行檔單獨拿到CentOS 7上也依然可以正常執行,如下圖:

musl-libc