作者:周秉誼 / 臺灣大學計算機及資訊網路中心作業管理組碩士後研究人員
以函式庫的方式共享程式碼,是Linux作業系統、嵌入式系統、與自由及開放原始碼軟體社群中,很常見的開發模式,可以充分利用現有的程式碼、大幅縮短程式開發週期。GNU開發工具可以協助程式設計人員完成從原始程式碼到二進制機械碼時各項煩瑣的工作,也能夠也能完成各種函式庫的建構,是程式開發的最佳工具。
前言
GNU開發工具 (GNU toolchain) 是Linux作業系統、嵌入式系統 (Embedded System)、與自由及開放原始碼軟體 (Free and Open Source Software, FOSS) 社群中,最常見、使用得最廣泛的程式開發工具。GNU開發工具是由GNU計劃 (GNU Project) 中的數個程式開發工具所集合而成,不但可以完成編譯 (compile)、組譯 (assemble) 和連結 (link) 等步驟、產生可執行的程式,還有自動化設定、自動化編譯、除錯工具等功能。除了用來產生可執行檔,GNU開發工具的另一個用途就是建構各種不同的函式庫 (library)。
函式庫
函式庫是由函數 (function)、副程式 (subroutine)、資料型態 (data type) 等功能性的程式片段組成的集合,用來讓不同程式共用、分享相同功能的程式片段,可以使程式開發也有類似模組化的概念。大部分需要編譯的程式語言,都會包括一個標準函式庫,提供程式語言中各個函數的功能,如C語言標準函式庫 (C Standard Library)。在產生可執行的程式時,就需要透過連結器 (linker) 來與使用到的函式庫進行連結,引入相關程式片段、或函式庫的資訊。在Linux作業系統、開放原始碼、自由軟體社群的開發環境中,相同功能的程式片段以函式庫的方式共享是非常常見的,不但可以充分利用現有的程式碼、縮短程式開發週期,也能夠降低程式維護的成本。GNU/Linux作業系統就是以GNU C函式庫 (GNU C Library, glibc) 和許多不同的函式庫建構出各種功能的程式和軟體。
傳統的函式庫會預先將程式碼編譯成目標檔 (object file),再將數個目標檔建檔 (archive) 成函式庫檔案。如果其他程式要使用函式庫的功能,就要在連結的時候到已經建檔的函式庫檔案找出使用到的程式片段,複製一份到程式的執行檔裡。這樣會在程式產生時就完成函式庫連結,所以就把這種函式庫稱為靜態函式庫 (static library)。
靜態函式庫十分簡單而直覺,但是有兩個缺點,複製程式片落到執行檔會讓檔案的大小變大、當函式庫有修改時程式需要重新進行連結。為了改善這個問題,就產生了共享函式庫 (shared library) 的概念,讓程式在執行時再動態連結 (dynamic linking) 使用到的函式庫,因此也稱為動態函式庫 (dynamic library)。要建構共享函式庫,在編譯目標檔時就要產生成位置獨立的機械碼 (position-independent code, PIC),方便未來執行時再決定函式呼叫的位址;在建立函式庫檔案時也要給定函式庫名稱,讓程式能夠找到需要的共享函式庫檔案。
建構函式庫通常要進行數個程式碼檔案的編譯、組譯並將數個目標檔建檔成函式庫檔案,因此可以利用GNU開發工具中的自動化編譯工具gmake,配合Makefile裡的規則來操作。使用GNU編譯器套件 (GCC) 把全部的程式碼編譯成目標檔,再用靜態函式庫工具ar建檔成靜態函式庫檔案、或用連結器ld建立共享函式庫。
BLAS函式庫
基礎線性代數程式集 (Basic Linear Algebra Subprograms, BLAS) 是一個線性代數運算的應用程式介面 (API),可以進行向量 (vector) 和矩陣 (matrix) 之間的乘法和加法運算,被重度使用在高效能運算領域。Top500超級電腦排名所用的LINPACK及HPL程式,就十分依賴BLAS中的DGEMM (Double-precision General Matrix Multiply) 副程式。Netlib的BLAS是一個基本而常見的實作,由Fortran 77寫成,共有150個Fortran的程式碼檔案,可以在Netlib的網站 http://netlib.org/blas 取得。以下就以建構Netlib的BLAS函式庫來說明如何使用GNU開發工具建構函式庫。
GNU Make自動化編譯工具
為了自動化地進行這麼多程式碼的編譯和函式庫建置,就要善加利用自動化編譯工具gmake。而要使用gmake,就要先撰寫Makefile。Makefile中的敘述可以分為三種,變數設定、編譯目標 (target)、及指令。
變數的內容通常用來指定編譯工具的名稱、工具的參數、相關函式庫路徑、要編譯的程式碼清單等等,可以用等號來設定變數的值,以錢號($)和小括號來使用變數。如FORTRAN= gfortran用來指定使用GCC中的gfortran編譯器、OPTS = -O3設定編譯器的參數。比較特別的是,FRC = $(shell ls *.f) 會呼叫系統中的ls指令把目錄中副檔名為.f的檔案列出來,設定給變數FRC,因此FRC就是需要編譯的程式碼清單。OBJ = $(FRC:.f=.o) 會把FRC中的程式碼檔副檔名替換成.o的副檔名,因此OBJ就是編譯產生的目標檔清單。
編譯目標用來分別不同層級或類型的指令操作,在呼叫make指令時可以加上編譯目標的名稱,就會直接進行該項操作。如指令make clean會執行Makefile裡名為clean的編譯目標,習慣上是用來清除編譯產生的目標檔、執行檔和函式庫檔案。在Makefile中每個編譯目標的名稱要從行首開始、以冒號結尾,在冒號之後可以加上相依的編譯目標,在進行某個編譯目標前,會先進行相依的編譯目標。如all: $(OBJ) 就會相依目標檔清單,而目標檔清單中的每一個項目,會依照.f.o:的編譯目標進行編譯後,再。.f.o的意思是副檔名.o的項目會相依於副檔名.f的檔案,通常會利用副檔名.f的同名檔案來產生副檔名.o的檔案。
Makefile中的指令敘述都要以tab移位8格的大空白開頭,才會被視為指令。指令敘述通常都是某個編譯目標的操作。如編譯目標.f.o的操作指令為$(FORTRAN) $(OPTS) -c $<,就是以變數FORTRAN設定的編譯器、和OPTS的內容加上-c為參數,來編譯產生副檔名.o的目標檔。其中$@代表副檔名.o的目標檔檔名,$<代表副檔名.f的程式碼檔名。編譯目標all的操作指令$(ARCH) $(ARCHFLAGS) libblas.a $(OBJ),就代表用ARCH變數定義的靜態函式庫工具,配合ARCHFLAGS裡的參數,由OBJ目標檔清單裡的檔案,來產生函式庫檔案libblas.a。
FORTRAN = gfortran
OPTS = -O3 -fPIC
ARCH = ar
ARCHFLAGS= cr
LINKER = gfortran
FRC = $(shell ls *.f)
OBJ = $(FRC:.f=.o)
all: $(OBJ)
$(ARCH) $(ARCHFLAGS) libblas.a $(OBJ)
$(LINKER) -shared -o libblas.so libblas.a
clean:
rm -f *.o libblas.a libblas.so
.f.o:
$(FORTRAN) $(OPTS) -c $<
|
表一 建構Netlib BLAS函式庫的Makefile檔案
Makefile檔案設定完成後,只要用make指令就可以很方便地產生函式庫的檔案了。make指令會自動尋找目錄內的Makefile檔案,如果有多個不同的Makefile檔要切換時,可以用-f參數指定檔名。還有一個能夠縮短建置時間的參數,-j參數可以指定同時有多少個編譯工作同時進行,在多核心的系統內可設為2到4,實際執行時間會有很明顯的加速。如果只有部份程式碼進行修改,make指令也會自動地比對程式碼的更動時間,只針對有更新的程式碼檔案重新進行編譯。
$ make -j 3
gfortran -O3 -fPIC -c caxpy.f
gfortran -O3 -fPIC -c ccopy.f
gfortran -O3 -fPIC -c cdotc.f
gfortran -O3 -fPIC -c cdotu.f
(...skip...)
gfortran -O3 -fPIC -c ztrmm.f
gfortran -O3 -fPIC -c ztrmv.f
gfortran -O3 -fPIC -c ztrsm.f
gfortran -O3 -fPIC -c ztrsv.f
ar cr libblas.a caxpy.o ccopy.o cdotc.o cdotu.o cgbmv.o cgemm.o cgemv.o cgerc.o cgeru.o chbmv.o
(...skip...)
gfortran -shared -o libblas.so libblas.a
$ file libblas.a
libblas.a: current ar archive
$ ldd libblas.so
libgfortran.so.1 => /usr/lib64/libgfortran.so.1 (0x0000002a9567f000)
libm.so.6 => /lib64/tls/libm.so.6 (0x0000002a95814000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x0000002a9599a000)
libc.so.6 => /lib64/tls/libc.so.6 (0x0000002a95aa6000)
/lib64/ld-linux-x86-64.so.2 (0x000000552aaaa000)
|
表二 以make指令建構BLAS函式庫
這些特性讓make指令在建構大型函式庫時,可以節省很多時間。以建構Netlib的BLAS函式庫為例,以make首次編譯,不使用-j參數,需要16.536秒;使用-j參數同時進行3個編譯工作,只要5.848秒;只更動一個程式碼檔案後重新編譯,只要0.471秒。
共享函式庫編譯與連結
當建構共享函式庫時,有幾點需要特別注意。GCC編譯器在編譯目標檔時,參數要加上-fPIC,讓GCC產生位置獨立機械碼的目標檔。否則在ld連結器要產生共享函式庫時,會無法正確替換相關函式呼叫的位址。進行連結時,還需要加上-L及-l參數來指定函式庫中會使用到的共享函式庫,執行時才能正確連結到其他共享函式庫。如果不需要對ld連結器進行參數設定,也可以直接使用GCC編譯器作為連結器,讓GCC幫忙呼叫連結器產生共享函式庫檔案。
在Linux作業系統中,可以使用ldd指令來查看每一個執行檔或共享函式庫的共享函式庫相依性。ldd會顯示出這個檔案需要的共享函式庫名稱,及目前系統中對應到的共享函式庫檔案和位置。其中最重要的共享函式庫就是ld-linux.so,所有Linux作業系統中的程式,只要不是靜態連結,就需要使用到ld-linux.so。它是一個動態連結器 (dynamic linker) 和動態載入器 (dynamic loader),程式會呼叫ld-linux.so來載入共享函式庫並進行連結。執行程式時無法正確連結到共享函式庫,就會發生未定義符號(undefined symbol)的錯誤,可以透過環境變數LD_LIBRARY_PATH來設定共享函式庫的路徑。
結語
在Linux作業系統、嵌入式系統、及開放原始碼、自由軟體社群中,各個不同的函式庫讓程式開發的週期縮短、讓程式設計人員可以利用現有的函式庫組合成各種不同的軟體,也讓自由及開放原始碼軟體的使用更加自由、更容易分享。GNU開發工具不只能協助程式設計人員完成從原始程式碼到可執行檔的每一個步驟,也能完成各種函式庫的建構,是程式開發的好選擇。