章節概述#

Linux 深受程式設計師喜愛,不僅因為擁有豐富的工具與環境,更因為系統本身有極佳的文件與透明度。即使你不是程式設計師,也應該了解開發工具,因為它們在 Linux 系統管理中扮演的角色比在其他作業系統中更為重要。

本章涵蓋 C compiler、make 建構系統、Lex/Yacc、scripting languages、Java,以及編譯語言的未來展望。其中 shared libraries 的討論是最重要的實務知識。


15.1 C Compiler#

基本編譯流程#

大多數 Linux 工具與應用程式都是用 CC++ 撰寫的。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 是處理器幾乎可以理解的二進位檔,但還需要透過 linkerld)將多個 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 librarylibc.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 需要知道四件事:

  1. 如何列出執行檔需要的 shared libraries
  2. 執行檔如何尋找 shared libraries
  3. 如何連結 shared library
  4. 如何避免常見問題

列出 shared library 依賴:

$ ldd /bin/bash

ld.so 尋找 shared library 的順序:

  1. 執行檔內建的 rpath(runtime library search path)
  2. 系統快取 /etc/ld.so.cache(由 /etc/ld.so.conf 設定)
  3. 環境變數 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 編譯管理工具,用於自動化建構流程。當你看到 Makefilemakefile,就知道這個專案使用 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:將編譯好的程式安裝到系統適當位置
  • testcheck:執行測試
  • depend:用 compiler -M 建立依賴關係
  • all:通常是第一個 target,建構所有執行檔

執行 make -n install 可先預覽 install 會做什麼,再決定是否實際執行。

15.2.8 Makefile Organization#

良好的 Makefile 組織方式:

  • 開頭放函式庫與 include 的巨集定義,按套件分組
  • Object files 依執行檔分組
  • 每個執行檔有獨立的規則,不要合併多個執行檔的規則

15.3 Lex and Yacc#

LexYacc 是用於編譯程式中讀取設定檔或命令的建構工具,是程式語言的基礎元件:

  • Lextokenizer(詞法分析器),將文字轉換為帶標籤的 tokens。GNU/Linux 版本叫 flex,連結時可能需要 -ll-lfl 旗標
  • Yaccparser(語法分析器),根據文法規則讀取 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)包含 javac compiler 與 jar 打包工具

15.6 展望:編譯套件#

編譯器與 scripting languages 的世界持續擴展:

  • 新的編譯語言如 Go(golang)和 Rust 在應用與系統程式開發中越來越受歡迎
  • LLVM compiler infrastructure 大幅降低了開發 compiler 的門檻
  • 掌握了系統上的程式工具基礎後,下一章將學習如何在 Linux 上從原始碼建構套件