綠色執行檔之製作

很多年前,軟體幾乎都是需要安裝的。這是說,需要跑個安裝程式,煞有其事地複製檔案,順便留下些莫名其妙的登錄資訊 (Windows registry)。 最扯的是,整個安裝的過程要作許多不明的檢查,造成了許多無所謂的時間浪費。

並不是說所有的程式都不需要這些檢查,也不是說他們都不需要登錄資訊,而是對於大部分的程式來說,只要滿足下面的條件就可以達成可攜帶化(綠化)

  • 執行時需要的程式庫安置在恰當的位置

    這需要了解作業系統的機制方可辦到。對軟體工程師來說也是其中最困難的項目

  • 執行時需要的資料以及設定檔安置在恰當的位置,或者可以透過彈性的方式來指定

    對軟體作者來說容易解決

  • 需要妥善設置依存的其他服務,像是資料庫引擎等

    乍看之下這點降低了軟體的可綠化性,但使用服務的方法可以透過設定檔來指定。當然,試著綠化相依服務也是可行的想法

若是軟體可以綠化成功,那對使用者來說,只要解開包裝就可以馬上使用軟體了;若不喜歡軟體,也可以馬上刪除,不留痕跡。對企業來說,佈署這類的軟體不需要特別撰寫、製作安裝程式,只要注意執行環境所造成的設定差異需要反映在附帶的設定檔內。

本文的主題著重在安置被編譯成機器碼執行檔的相依程式庫的方法,不過理論上其他具備像是 JVM 的虛擬機的程式語言也可以依據上面的過程做轉換來達成綠化。

注意事項

本文討論的可綠化,只限於把軟體移動到平台相同的作業系統上運作。例如說,綠化後的 linux x86-64 軟體,依然只能在 linux x86-64 上執行,無法在 Windows x86-64, 也無法在 linux mips 上直接執行。

Windows 的程式庫安置方式

簡單的說,只要把必要的動態連結程式庫放到和執行檔相同的目錄下即可。和執行檔處在同目錄下的動態連結程式庫會被優先使用,接著才是系統區下的程式庫。

需要注意像是 Side-by-side dependency 管理系統下的元件,像是 msvcr80.dll 之類的程式庫,微軟建議讓使用者預先安裝。

另外,像是 Qt 之類有 pluging 系統的框架,需要注意其找尋 plugin 的規則,或者必須由開發者把 plugin 編入 Qt 的程式庫本身內。

Linux 的程式庫安置方式

Linux 的情況,有兩種方式: 使用 LD_LIBRARY_PATH 或是直接調用動態連結器。

首先說明使用 LD_LIBRARY_PATH 的方式。簡單的說,若路徑結構如下

1
2
3
4
5
6
7
./bin
the-binary
./lib
libc.so
libd.so
ld-custom.so
./launch.sh

那麼 launch.sh 的內容就可以是

1
2
3
# Code for setting up $APP_ROOT is ignored in this code
# ...
LD_LIBRARY_PATH="$APP_ROOT/lib" $APP_ROOT/bin/the-binary $@

程式執行中,可以利用 /proc/$PID/mapped_files 目錄下的資訊來看利用 memory mapping 載入的檔案,包括動態連結程式庫以及使用的連結器。

總之,利用這個方法,軟體 the-binary 就可以使用 ./lib 之下的相依程式庫。這個方法美中不足的地方在於,必須使用系統的動態連結程式 ld*.so

那若想要使用自帶的動態連結程式怎麼辦呢?其實 linux 下的動態連結程式是一個可執行檔!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ /lib64/ld-linux-x86-64.so.2
Usage: ld.so [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]
You have invoked `ld.so', the helper program for shared library executables.
This program usually lives in the file `/lib/ld.so', and special directives
in executable files using ELF shared libraries tell the system's program
loader to load the helper program from this file. This helper program loads
the shared libraries needed by the program executable, prepares the program
to run, and runs it. You may invoke this helper program directly from the
command line to load and run an ELF executable file; this is like executing
that file itself, but always uses this helper program from the file you
specified, instead of the helper program file specified in the executable
file you run. This is mostly of use for maintainers to test new versions
of this helper program; chances are you did not intend to run this program.
--list list all dependencies and how they are resolved
--verify verify that given object really is a dynamically linked
object we can handle
--inhibit-cache Do not use /etc/ld.so.cache
--library-path PATH use given PATH instead of content of the environment
variable LD_LIBRARY_PATH
--inhibit-rpath LIST ignore RUNPATH and RPATH information in object names
in LIST
--audit LIST use objects named in LIST as auditors

也就是動態連結器可以讓使用者自行指定程式庫的位置。利用這樣的規則,上面的 launch.sh 也可寫成

1
2
3
# Code for setting up $APP_ROOT is ignored in this code
# ...
$APP_ROOT/lib/ld-custom.so $APP_ROOT/bin/the-binary $@

可能有人會想,為何有人想要使用自己的動態連結程式呢。其實,若程式使用的 C 語言程式庫不同,那動態連結器就可能具些許的差異。像是,在 OpenSUSE Leap 42.3 下直接編譯的 Rust 執行檔無法直接在 Alpine linux 下執行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
suseSh $ ldd rust-quickxml-events
linux-vdso.so.1 (0x00007ffe593f3000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f7ea7699000)
librt.so.1 => /lib64/librt.so.1 (0x00007f7ea7491000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f7ea7274000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f7ea705d000)
libc.so.6 => /lib64/libc.so.6 (0x00007f7ea6cba000)
/lib64/ld-linux-x86-64.so.2 (0x00007f7ea800e000)
suseSh $ scp rust-quickxml-events me@theAlpine:~
suseSh $ ssh me@theAlpine
alpineSh $ ls rust-quickxml-events
rust-quickxml-events
alpineSh $ ./rust-quickxml-events
-ash: ./rust-quickxml-events: not found
alpineSh $ /lib/ld-musl-x86_64.so.1 ./rust-quickxml-events
Error loading shared library ld-linux-x86-64.so.2: No such file or directory (needed by ./rust-quickxml-events)
Error relocating ./rust-quickxml-events: __register_atfork: symbol not found
Error relocating ./rust-quickxml-events: __rawmemchr: symbol not found

從上面的 shell session 應該可以得知,首先在 alpine 執行建立於 OpenSUSE 的執行檔會發生詭異的錯誤(找不到檔案),可是根據 ls 檔案明明就在那裡。接著,利用 alpine 的動態連結程式來載入的話會發現錯誤。

但若是把相依元件以及 OpenSUSE 的動態連結程式也移動過去的話

1
2
3
4
5
6
7
8
alpineSh $ ls
ld-linux-x86-64.so.2 libdl.so.2 librt.so.1 redirect-probe-tui
libc.so.6 libgcc_s.so.1 libssl.so.1.0.0 rust-quickxml-events
libcrypto.so.1.0.0 libpthread.so.0 libz.so.1
alpineSh $ ./rust-quickxml-events
-ash: ./rust-quickxml-events: not found
alpineSh $ ./ld-linux-x86-64.so.2 --library-path . ./rust-quickxml-events
# program is now waiting for input

程式就可以執行了耶!!

為什麼這個方法行呢?因為作業系統在程式啟動的時候,會替需要進行動態載入的程式,依照程式中的 interp header 來載入動態連結程式來進行動態連結程式庫的載入工作。完工後程式才會開始執行。因為 interp 是寫死在程式內的(雖說可以用 patchelf 來修改),所以若兩個 linux distribute 使用的 C library 差異太大的話,就可能必須要連著動態連結程式一起般過去才可以解決程式搬移的問題。

利用動態載入程式來執行程式有個小缺點,就是執行出來的 process name 很可能是動態載入程式的名稱,而非本來要執行程式的名稱。解決的方法是,把動態載入程式 複製 一份,或是建立軟連結 (soft link by ln -s) 成為自己想要的名字,然後呼叫那個複製品來載入你的程式。

結論

綠化軟體的其中一個條件是佈置動態連結程式庫。

對 Windows 來說,除了一些例外( Windows Sxs, plugin system ),大多數的情況下只要把程式庫和程式放在相同的資料夾中就好。

對 Linux 來說,若 C 語言程式庫,或是動態載入程式相同,則可以利用 LD_LIBRARY_PATH 來執行程式;若無法保證 C 語言程式庫的版本,則可以考慮連帶投放 ld-*.so

文章目錄
  1. 1. 注意事項
  2. 2. Windows 的程式庫安置方式
  3. 3. Linux 的程式庫安置方式
  4. 4. 結論