章節概述#
Linux 深受程式設計師喜愛,不僅因為擁有豐富的工具與環境,更因為系統本身有極佳的文件與透明度。即使你不是程式設計師,也應該了解開發工具,因為它們在 Linux 系統管理中扮演的角色比在其他作業系統中更為重要。
本章涵蓋 C compiler、make 建構系統、Lex/Yacc、scripting languages、Java,以及編譯語言的未來展望。其中 shared libraries 的討論是最重要的實務知識。
15.1 C Compiler#
基本編譯流程#
大多數 Linux 工具與應用程式都是用 C 或 C++ 撰寫的。C 程式遵循傳統開發流程:撰寫原始碼、編譯成二進位執行檔。
- Linux 上的 C compiler 主要是 gcc(GNU C Compiler),較新的 clang(來自 LLVM 專案)也逐漸普及
- C 原始碼檔案以
.c結尾 - 最簡單的編譯方式:
$ cc hello.c
$ cc -o hello hello.c # 指定輸出檔名大多數發行版預設不包含 C compiler。Debian/Ubuntu 可安裝
build-essential套件,Fedora/CentOS 可用yum groupinstall "Development Tools"。
15.1.1 編譯多個原始檔#
實際的 C 程式通常分成多個檔案。編譯時使用 -c 選項產生 object files(.o 檔),然後再連結成執行檔:
$ cc -c main.c # 產生 main.o
$ cc -c aux.c # 產生 aux.o
$ cc -o myprog main.o aux.o # 連結成執行檔- Object file 是處理器幾乎可以理解的二進位檔,但還需要透過 linker(
ld)將多個 object file 與系統函式庫結合成完整程式 - 實務上很少直接呼叫
ld,因為 C compiler 知道如何呼叫 linker
15.1.2 連結函式庫 (Linking with Libraries)#
僅靠 compiler 處理原始碼通常不夠,還需要 libraries(函式庫)來建構完整程式。Library 本質上是預先編譯好的 object file 集合。
- 連結時使用
-l選項指定函式庫名稱 - 使用
-L選項指定非標準函式庫路徑
$ cc -o badobject badobject.o -lcurses
$ cc -o badobject badobject.o -lcurses -L/usr/junk/lib -lcrud- 遇到
undefined reference錯誤通常表示缺少函式庫 - C standard library(
libc.a)會自動連結,無需手動指定
若要搜尋某個函式屬於哪個函式庫,可用
nm --defined-only libcurses.a查看函式庫中定義的符號。
15.1.3 Shared Libraries(共享函式庫)#
以 .a 結尾的是 static library(靜態函式庫),linker 會將函式庫程式碼複製到執行檔中。Shared library(共享函式庫)則解決了靜態函式庫浪費磁碟空間與記憶體的問題:
- Shared library 連結時不複製程式碼,只加入 references
- 執行時由
ld.so(runtime dynamic linker/loader)載入所需的 shared library - 多個程序可共用同一份記憶體中的 shared library
- 更新函式庫時通常不需重新編譯程式
管理 shared library 需要知道四件事:
- 如何列出執行檔需要的 shared libraries
- 執行檔如何尋找 shared libraries
- 如何連結 shared library
- 如何避免常見問題
列出 shared library 依賴:
$ ldd /bin/bashld.so 尋找 shared library 的順序:
- 執行檔內建的 rpath(runtime library search path)
- 系統快取
/etc/ld.so.cache(由/etc/ld.so.conf設定) - 環境變數
LD_LIBRARY_PATH
# 修改 ld.so.conf 後需重建快取
# ldconfig -v連結 shared library 並設定 rpath:
$ cc -o myprog myprog.o -Wl,-rpath=/opt/obscure/lib -L/opt/obscure/lib -lweird絕對不要在 shell startup 檔或編譯軟體時設定
LD_LIBRARY_PATH。這會導致效能下降、函式庫衝突與版本不匹配等問題。若必須使用,應撰寫 wrapper script 限制其影響範圍。
15.1.4 Header (Include) Files 與目錄#
Header files(.h 檔)包含型別宣告與函式庫函式宣告。預設的 include 目錄是 /usr/include。
- 使用
-I選項指定額外的 include 目錄:
$ cc -c -I/usr/junk/include badinclude.c- 使用
< >括住的 include 表示在系統目錄中搜尋 - 使用
" "括住的 include 表示檔案在與原始碼相同的目錄中
C Preprocessor 是 compiler 在解析程式碼之前執行的程式,處理三種 directives(指令):
#include:引入檔案#define:巨集定義(也可透過 compiler 的-D選項定義)#ifdef/#if/#endif:條件編譯
15.2 make#
make 是傳統的 Unix 編譯管理工具,用於自動化建構流程。當你看到 Makefile 或 makefile,就知道這個專案使用 make。
15.2.1 Makefile 範例#
Makefile 的核心概念:
- Target(目標):想要建構的東西(檔案、標籤等)
- Dependencies(依賴):target 所需的前置條件
- Rule(規則):如何從依賴建構目標

Figure 15-1: Makefile dependencies
# Makefile 範例
OBJS=aux.o main.o
all: myprog
myprog: $(OBJS)
$(CC) -o myprog $(OBJS)Makefile 中規則的命令前必須使用 tab 縮排,不能用空格。若出現
missing separator錯誤,通常就是 tab 的問題。
15.2.2 Built-in Rules(內建規則)#
make 有內建規則,例如它知道如何從 .c 檔產生 .o 檔(自動執行 cc -c)。甚至不需要 Makefile,make 就能嘗試建構簡單程式:
$ make blah # 如果有 blah.c,make 會自動編譯15.2.3 最終程式建構#
$(CC) 巨集展開為 compiler 名稱,make 用它來連結 object files 成最終執行檔。
15.2.4 Dependency Updates(依賴更新)#
make 的目標是只執行必要的最少步驟來更新 target。它透過比較檔案時間戳判斷哪些檔案需要重新編譯。連續執行兩次 make,第二次會輸出:
make: Nothing to be done for 'all'.15.2.5 Command-Line Arguments and Options#
- 在命令列指定單一 target:
make aux.o - 在命令列定義巨集:
make CC=clang -n:只列出會執行的命令,不實際執行(dry run)-f file:指定使用其他 Makefile
15.2.6 Standard Macros and Variables#
常用巨集:
CFLAGS:C compiler 選項LDFLAGS:linker 選項LDLIBS:函式庫名稱選項CC:C compiler(預設cc)CPPFLAGS:C preprocessor 選項CXXFLAGS:C++ compiler 選項
常用自動變數:
$@:目前的 target$<:第一個 dependency$*:目前 target 的 basename(stem)
15.2.7 Conventional Targets(慣例目標)#
clean:移除所有 object files 與執行檔distclean:移除所有非原始發佈的檔案(含 Makefile 本身)install:將編譯好的程式安裝到系統適當位置test或check:執行測試depend:用 compiler-M建立依賴關係all:通常是第一個 target,建構所有執行檔
執行
make -n install可先預覽 install 會做什麼,再決定是否實際執行。
15.2.8 Makefile Organization#
良好的 Makefile 組織方式:
- 開頭放函式庫與 include 的巨集定義,按套件分組
- Object files 依執行檔分組
- 每個執行檔有獨立的規則,不要合併多個執行檔的規則
15.3 Lex and Yacc#
Lex 和 Yacc 是用於編譯程式中讀取設定檔或命令的建構工具,是程式語言的基礎元件:
- Lex:tokenizer(詞法分析器),將文字轉換為帶標籤的 tokens。GNU/Linux 版本叫 flex,連結時可能需要
-ll或-lfl旗標 - Yacc:parser(語法分析器),根據文法規則讀取 tokens。GNU 版本叫 bison,使用
bison -y取得 Yacc 相容性,連結時可能需要-ly旗標
15.4 Scripting Languages#
Shell scripts 仍然是 Unix 的重要部分,但許多系統程式已從 C 轉向 scripting languages。
腳本基礎#
- 腳本的第一行是 shebang(
#!),指定直譯器路徑 - 例如 Python 腳本:
#!/usr/bin/python或#!/usr/bin/env python
腳本第一行最常見的問題是直譯器路徑無效。例如若
tail實際在/bin而非/usr/bin,就會出現bad interpreter: No such file or directory錯誤。
15.4.1 Python#
- 功能強大的 scripting language,擅長文字處理、資料庫存取、網路、多執行緒
- 有強大的互動模式與良好的物件導向模型
- 執行檔通常在
/usr/bin,廣泛用於從資料分析到 web 應用
15.4.2 Perl#
- 較早期的第三方 Unix scripting language,被稱為程式工具的「瑞士刀」
- 擅長文字處理、格式轉換、檔案操作
- 近年來部分領域已被 Python 取代,但仍有許多工具使用 Perl
15.4.3 Other Scripting Languages#
- PHP:超文本處理語言,常見於動態網頁
- Ruby:物件導向語言,受 web 開發者歡迎
- JavaScript:主要用於瀏覽器,Node.js 讓它也能用於伺服器端
- Emacs Lisp:Emacs 編輯器使用的 Lisp 變體
- MATLAB / Octave:數學矩陣運算語言
- R:統計分析語言
- Mathematica:商業數學程式語言
- m4:巨集處理語言,主要用於 GNU autotools
- Tcl:簡單的 scripting language,常搭配 Tk GUI toolkit 與 Expect 自動化工具
15.5 Java#
Java 是一種編譯語言,語法比 C 簡單,對物件導向程式設計有強力支援。常用於 web 應用與 Android 開發。
- Java 有兩種 compiler:native compiler(產生機器碼)與 bytecode compiler(產生 bytecode)
- 實務上幾乎都使用 bytecode
- Bytecode 檔以
.class結尾,執行需要 JRE(Java Runtime Environment) .jar檔是.class檔的封裝檔
$ java file.class # 執行 bytecode
$ java -jar file.jar # 執行 jar 檔
$ javac file.java # 編譯 Java 原始碼- 可能需要設定
JAVA_HOME環境變數 CLASSPATH類似PATH,指定 class 檔案的搜尋目錄- JDK(Java Development Kit)包含
javaccompiler 與jar打包工具
15.6 展望:編譯套件#
編譯器與 scripting languages 的世界持續擴展:
- 新的編譯語言如 Go(golang)和 Rust 在應用與系統程式開發中越來越受歡迎
- LLVM compiler infrastructure 大幅降低了開發 compiler 的門檻
- 掌握了系統上的程式工具基礎後,下一章將學習如何在 Linux 上從原始碼建構套件