跳到內容

靜態連結

Crystal 支援靜態連結,也就是說,它可以將二進制檔案與靜態函式庫連結,這樣這些函式庫就不需要作為執行時期依賴存在。這提高了可攜性,但代價是二進制檔案會比較大。

可以使用 --static 編譯器標誌啟用靜態連結。請參閱語言參考中的使用說明

當給定 --static 時,會啟用靜態函式庫的連結,但它不是排他的。如果函式庫的動態版本在編譯器的函式庫查找鏈中比靜態版本高(或者如果完全沒有靜態函式庫),則產生的二進制檔案將不會是完全靜態連結的。為了建構一個靜態二進制檔案,您需要確保已連結函式庫的靜態版本可用,並且編譯器可以找到它們。

編譯器使用 CRYSTAL_LIBRARY_PATH 環境變數作為要連結的靜態和動態函式庫的第一個查找目的地。這可以用來提供也以動態函式庫形式提供的函式庫的靜態版本。

並非所有函式庫都適合靜態連結,因此可能會有一些問題。例如,openssl 以複雜性而聞名,glibc 也是如此(請參閱完全靜態連結)。

一些套件管理器提供靜態函式庫的特定套件,其中 foo 提供動態函式庫,而 foo-static 例如提供靜態函式庫。有時靜態函式庫也包含在開發套件中。

完全靜態連結

一個完全靜態連結的程式沒有任何動態函式庫依賴。這對於交付可攜式、預先編譯的二進制檔案很有用。完全靜態連結 Crystal 程式的突出例子是官方發行套件中的 crystalshards 二進制檔案。

為了完全靜態連結一個程式,所有依賴都需要在編譯時以靜態函式庫的形式提供。有時這可能會很棘手,尤其是常見的 libc 函式庫。

Linux

glibc

glibc 是 Linux 系統上最常見的 libc 實作。不幸的是,它不適合靜態連結,而且非常不建議這樣做。

相反,建議在 Linux 上針對 musl-libc 進行靜態連結。由於它是靜態連結的,因此針對 musl-libc 連結的二進制檔案也將在 glibc 系統上執行。這就是它的全部重點。

然而,除了動態連結的 glibc 之外,靜態連結其他函式庫是完全可以的。

musl-libc

musl-libc 是一個乾淨、高效的 libc 實作,具有出色的靜態連結支援。

建構靜態連結 Crystal 程式的建議方法是使用 Alpine Linux,這是一個基於 musl-libc 的最小 Linux 發行版。

官方的 基於 Alpine Linux 的 Docker 映像檔可在 Docker Hub 上取得,網址為 crystallang/crystal。最新的發行版標記為 crystallang/crystal:latest-alpine。Dockerfile 原始碼可在 crystal-lang/distribution-scripts 上取得。

這些 Docker 映像檔預先安裝了 crystal 編譯器、shards 以及所有 stdlib 依賴項的靜態函式庫,即使是基於 glibc 的系統,也可以輕鬆建構靜態 Crystal 二進制檔案。Linux 的官方 Crystal 編譯器版本是使用這些映像檔建立的。

以下是一個範例,說明如何使用 Docker 映像檔來建構靜態連結的 Hello World 程式

$ echo 'puts "Hello World!"' > hello-world.cr
$ docker run --rm -it -v $(pwd):/workspace -w /workspace crystallang/crystal:latest-alpine \
    crystal build hello-world.cr --static
$ ./hello-world
Hello World!
$ ldd hello-world
        statically linked

Alpine 的套件管理器 APK 也很容易使用來安裝靜態函式庫。可在 pkgs.alpinelinux.org 上找到可用的套件。

macOS

macOS 不 官方支援完全靜態連結,因為所需的系統函式庫不以靜態函式庫的形式提供。

Windows

Windows 不支援完全靜態連結,因為 Win32 函式庫不以靜態函式庫的形式提供。

目前,靜態連結是 Windows 上的預設連結模式,可以透過 -Dpreview_dll 編譯時期標誌選擇加入動態連結。為了區分靜態函式庫和 DLL 匯入函式庫,當編譯器在給定目錄中搜尋函式庫 foo.lib 時,將在靜態連結時首先嘗試 foo-static.lib,而在動態連結時首先嘗試 foo-dynamic.lib。官方 Windows 套件會分發所有第三方依賴項的靜態和 DLL 匯入函式庫,LLVM 除外。

靜態連結表示使用 Microsoft C 執行時期函式庫的靜態版本 (/MT),而動態連結表示使用動態版本 (/MD);應考慮使用此方法建構額外的 C 函式庫,以避免連結器出現關於混合 CRT 版本的警告。目前沒有辦法在靜態連結時使用動態 CRT。

識別靜態依賴

如果要靜態連結依賴項,您需要有其靜態函式庫可用。大多數系統預設不會安裝靜態函式庫,因此您需要明確安裝它們。首先,您必須知道您的程式連結了哪些函式庫。

注意

靜態函式庫在 POSIX 系統上的副檔名為 .a,在 Windows 上則為 .lib。Windows 上的 DLL 匯入函式庫也使用 .lib 副檔名。動態函式庫在 Linux 和大多數其他 POSIX 平台上使用 .so,在 macOS 上使用 .dylib,而在 Windows 上使用 .dll

在大多數 POSIX 系統上,工具 ldd 會顯示可執行檔連結到哪些動態函式庫。macOS 上對應的工具是 otool -L,而 Windows 上對應的工具是 dumpbin /dependents

以下範例顯示在 Ubuntu 18.04 LTS 上(在 crystallang/crystal:0.36.1 docker 映像檔中),使用 Crystal 0.36.1 和 LLVM 10.0 建置的簡單 Hello World 程式的 ldd 輸出。結果在其他系統和版本上會有所不同。

$ ldd hello-world_glibc
    linux-vdso.so.1 (0x00007ffeaf990000)
    libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007fc393624000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fc393286000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fc393067000)
    libevent-2.1.so.6 => /usr/lib/x86_64-linux-gnu/libevent-2.1.so.6 (0x00007fc392e16000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fc392c12000)
    libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fc3929fa000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc392609000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fc393dde000)

這些函式庫是 Crystal 標準函式庫的最低依賴項。即使是空程式也需要這些函式庫來設定 Crystal 執行環境。

看起來很多,但這些函式庫大多數實際上是 libc 發行版的一部分。

在 Alpine Linux 上,列表要小得多,因為 musl 將更多符號直接包含在單個二進制文件中。以下範例顯示在 Alpine Linux 3.12 上(在 crystallang/crystal:0.36.1-alpine docker 映像檔中),使用 Crystal 0.36.1 和 LLVM 10.0 建置的相同程式的輸出。

$ ldd hello-world_musl
    /lib/ld-musl-x86_64.so.1 (0x7fe14b05b000)
    libpcre.so.1 => /usr/lib/libpcre.so.1 (0x7fe14af1d000)
    libgc.so.1 => /usr/lib/libgc.so.1 (0x7fe14aead000)
    libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x7fe14ae99000)
    libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7fe14b05b000)

個別的函式庫為 libpcrelibgc,其餘的是 musl (libc)。在 Ubuntu 範例中也使用了相同的函式庫。

為了靜態連結這個程式,我們需要這三個函式庫的靜態版本。

注意

*-alpine docker 映像檔隨附標準函式庫使用的所有函式庫的靜態版本。如果您的程式沒有連結其他函式庫,那麼在建置命令中加入 --static 標誌就足以進行完全靜態連結。

動態函式庫查詢

動態函式庫在執行時的查詢路徑可以透過編譯期間的 CRYSTAL_LIBRARY_RPATH 環境變數來控制。目前 Linux 和 Windows 上都支援此功能。

Linux

如果在編譯期間定義了 CRYSTAL_LIBRARY_RPATH,它會透過 -Wl,rpath 選項不經修改地傳遞給連結器。確切的行為取決於連結器;通常,這會附加到 ELF 可執行檔的 DT_RUNPATHDT_RPATH 動態標籤條目。並非所有平台都支援特殊的 $ORIGIN / $LIB / $PLATFORM 變數。

Windows

標準函式庫支援實驗性的DLL 延遲載入,並可能透過延遲載入來變更 DLL 的搜尋順序。

如果針對給定的 DLL 傳遞 /DELAYLOAD 連結器標誌,則可執行檔第一次從該 DLL 載入符號時,它會先嘗試執行檔 CRYSTAL_LIBRARY_RPATH 中以分號分隔的路徑,按照它們宣告的順序,然後再嘗試預設的查詢順序CRYSTAL_LIBRARY_RPATH 內的 $ORIGIN 會展開為正在執行的執行檔本身的路徑。例如,如果在編譯期間 CRYSTAL_LIBRARY_RPATH=$ORIGIN\mylibs;C:\bar,並且提供了 --link-flags=/DELAYLOAD:calc.dll,且可執行檔位於 C:\foo\test.exe,那麼可執行檔會先搜尋 C:\foo\mylibs\calc.dll,然後是 C:\bar\calc.dll,之後再使用預設的順序。

非延遲載入的 DLL 會在程式啟動時立即載入,並且不遵循 CRYSTAL_LIBRARY_RPATH

預設情況下,不會延遲載入任何 DLL。但是,如果在編譯時指定了 -Dpreview_win32_delay_load 編譯時標誌,編譯器會從其匯入函式庫中偵測所有 DLL 相依性,在編譯期間為每個 DLL 插入 /DELAYLOAD 連結器標誌。