系統程式、連結器與載入器 (System Programs, Linkers, and Loaders)
本系列文章內容參考自經典教材 Operating System Concepts, 10th Edition (Silberschatz, Galvin, Gagne)。本文對應章節:Section 2.4 System Services、2.5 Linkers and Loaders、2.6 Why Applications Are Operating-System Specific。
2.4 系統服務 (System Services)
在 2.1 節,我們看到 OS 透過系統呼叫對外提供各種服務。但對於大多數使用者而言,他們從來不會直接呼叫 open()、read() 這類系統呼叫,他們使用的是更高一層的東西:系統程式 (System Programs),又稱系統工具 (System Utilities)。
系統程式是隨 OS 一起提供的一組便利工具,為程式的開發與執行提供一個友善的環境。有些系統程式只是在系統呼叫上包了一層外殼(如 cp、ls),有些則複雜許多,需要大量的應用邏輯(如文字編輯器、編譯器)。從使用者的角度來看,使用者對 OS 的印象,幾乎完全由這些系統程式決定,而不是由底層的系統呼叫決定。
七大類系統程式
系統程式可以按功能分成七大類:
檔案管理 (File Management):建立、刪除、複製、重新命名、列印、列示、存取與管理檔案和目錄。UNIX 的 cp、mv、rm、ls 都屬於這一類。
狀態資訊 (Status Information):向使用者回報系統狀態,包括日期、時間、可用記憶體與磁碟空間、使用者數量等。較複雜的工具提供詳細的效能分析、日誌記錄、除錯資訊。在多數系統中,這些工具的輸出會格式化後印到終端機、GUI 視窗或日誌檔中。部分系統還提供登錄檔 (Registry),用於儲存和查詢系統的配置資訊(Windows 的 Registry 是最典型的例子)。
檔案修改 (File Modification):可供建立與修改儲存裝置上檔案內容的文字編輯器(如 vi、nano、Notepad),以及搜尋檔案內容或執行文字轉換的特殊指令(如 grep、sed、awk)。
程式語言支援 (Programming-Language Support):常見程式語言(C、C++、Java、Python)的編譯器 (Compiler)、組譯器 (Assembler)、除錯器 (Debugger) 和直譯器 (Interpreter),通常隨 OS 一起提供,或可透過套件管理器另行下載。
程式載入與執行 (Program Loading and Execution):程式被編譯或組譯後,必須被載入記憶體才能執行。OS 提供絕對載入器 (Absolute Loader)、可重定位載入器 (Relocatable Loader)、鏈結編輯器 (Linkage Editor) 等工具,以及針對高階語言或機器語言的除錯系統。本文 2.5 節將詳細介紹連結器與載入器的完整工作流程。
通訊 (Communications):在行程、使用者和電腦系統之間建立虛擬連線 (Virtual Connections) 的工具。使用者可以透過這類工具傳送訊息到其他使用者的螢幕、瀏覽網頁、收發電子郵件、遠端登入其他機器,或在機器之間傳輸檔案。
背景服務 (Background Services):許多系統程式在開機時就被啟動,在背景持續執行,直到系統關機。這類長期執行的系統程式稱為服務 (Services)、子系統 (Subsystems) 或精靈 (Daemons)。
Daemon 是在背景持續執行、等待請求的系統程式,使用者通常感知不到它們的存在。典型例子包括:
- 網路連線監聽精靈:持續等待網路連線請求,將請求轉發給對應的行程(2.3.3.5 節提到的 network daemon)
- 排程精靈:依照預定時間表啟動行程(如 Linux 的
cron) - 系統錯誤監控服務:監聽並記錄系統錯誤事件
- 列印伺服器:管理列印佇列與印表機資源
一台典型系統同時執行數十個 Daemon。此外,某些 OS 會選擇在 User Mode(而非 Kernel Mode)執行一些重要活動,這時就需要 Daemon 來代為執行這些活動。
應用程式 (Application Programs)
除了系統程式,多數 OS 還附帶一批解決常見問題的應用程式 (Application Programs):網頁瀏覽器、文字處理器、試算表、資料庫系統、編譯器、統計分析套件、遊戲等。
這些應用程式不屬於 OS 的一部分,但它們進一步豐富了使用者對「這個 OS」的整體印象。以 macOS 為例,使用者看到的不只是系統呼叫和核心,而是整個 Aqua GUI 環境、Finder、Safari 等一整套應用程式生態。
2.5 連結器與載入器 (Linkers and Loaders)
一支程式在執行之前, 必須完成一條從原始碼 (Source Code) 到記憶體中的執行實體的完整轉換流程。這條流程由三個工具協力完成:編譯器 (Compiler)、連結器 (Linker)、載入器 (Loader)。
為什麼需要這條流程?
原始碼以人類可讀的高階語言撰寫,但 CPU 只能執行機器指令。編譯解決了「如何將高階語言轉換成機器碼」的問題。但轉換後的產物(物件檔)還不能直接執行,因為它不知道自己最終會被放到記憶體的哪個位置,也不知道它依賴的函式庫在哪裡。連結器負責把這些分散的物件檔整合成一個完整的可執行檔,並解析彼此之間的符號引用。最後,載入器才真正把可執行檔搬進記憶體,讓 CPU 開始執行。
下圖展示了從原始碼到記憶體中的程式,整個流程的每個環節:
圖中左側是工件(Artifact)的流動路徑:原始碼 main.c 透過編譯器生成物件檔 main.o,物件檔與其他物件檔一起進入連結器,生成可執行檔 main,可執行檔再透過載入器(加上執行期才動態連結的函式庫)進入記憶體。右側是對應的實際指令。每個箭頭代表一次「generates」的動作,每個步驟都是不可跳過的前置條件。
三個步驟的詳細說明
步驟一:編譯 (Compile)
編譯器(如 gcc -c main.c)將原始碼轉換成可重定位物件檔 (Relocatable Object File),例如 main.o。「可重定位」的意思是:這個物件檔被設計成可以載入到任意實體記憶體位址,而不預先假設自己在記憶體中的確切位置。這個設計使得物件檔可以被靈活地重新排列組合。
步驟二:連結 (Link)
連結器(如 gcc -o main main.o -lm)接收一個或多個可重定位物件檔,以及所需的函式庫(如數學函式庫 -lm),將它們合併成一個單一的二進位可執行檔 (Binary Executable File)。
連結過程中最重要的操作是重定位 (Relocation):連結器將最終的記憶體位址分配給程式的各個部分(函式、全域變數等),並修改程式碼與資料中所有的位址引用,使它們指向正確的最終位址。舉例來說,若 main.o 中呼叫了 sqrt() 函式,連結器必須找到 sqrt() 在數學函式庫中的確切位址,並把這個呼叫指向那個位址。
這裡有一個關鍵細節:連結器分配的位址是虛擬位址 (Virtual Address),而不是實體記憶體中的確切位置。
現代 OS 透過虛擬記憶體 (Virtual Memory) 機制,讓每個行程都擁有一塊獨立的「假想位址空間」,行程只看到虛擬位址。OS 在執行時期負責把這些虛擬位址對應 (Mapping) 到實際的實體記憶體位址,而這個對應關係每次執行都可以不同。
因此,同一個可執行檔(例如從 USB 複製給另一台相同 OS、相同 CPU 架構的電腦)是可以直接執行的,不需要重新編譯。兩台電腦的實體記憶體分配方式可以完全不同,但 OS 都會把同一組虛擬位址正確地對應到各自空閒的實體記憶體上。虛擬記憶體的完整機制會在第 9 章詳細討論。
步驟三:載入與執行 (Load and Run)
在 UNIX/Linux 系統上,當使用者在命令列輸入 ./main 時,Shell 的行為如下:
- Shell 呼叫
fork()建立一個新的子行程 - 子行程呼叫
exec()並傳入可執行檔名稱 - 載入器 (Loader) 接手,將可執行檔載入新行程的位址空間
- 程式開始執行
當使用者在 GUI 環境下雙擊一個程式圖示時,系統同樣會在背後呼叫載入器,機制與命令列的 ./main 完全相同,只是觸發的方式不同。
動態連結 (Dynamic Linking)
上述流程假設所有函式庫在連結時就被整合進可執行檔,稱為靜態連結 (Static Linking)。但多數現代系統支援另一種策略:動態連結 (Dynamic Linking),即把連結的動作推遲到程式載入或甚至執行的時候。
以 Figure 2.11 中的例子為例,數學函式庫並沒有被連結進可執行檔 main。連結器只在 main 中插入了重定位資訊 (Relocation Information),告訴載入器「這個函式庫在執行時才需要動態連結和載入」。當 main 被載入並執行時,若真正用到了數學函式庫中的函式,載入器才去找到並載入這個函式庫。
動態連結帶來了幾個重要好處:
- 節省磁碟與記憶體空間:若某個函式庫根本沒被用到,就完全不需要把它的程式碼包進來。若多個行程都需要同一個動態函式庫,記憶體中只需要一份副本,所有行程可以共用,大幅節省記憶體(詳見第 9 章)。
- 彈性更新:函式庫升級後,所有依賴它的程式下次執行時自動使用新版本,無需重新編譯。
Windows 將這種動態函式庫稱為 DLL(Dynamically Linked Libraries),副檔名為 .dll;Linux 則使用共享物件 (Shared Object),副檔名為 .so;macOS 使用 .dylib。
可執行檔格式:ELF、PE、Mach-O
物件檔和可執行檔都遵循特定的標準格式,這些格式規定了機器碼、符號表 (Symbol Table) 及各種中繼資料的排列方式,讓 OS 知道如何正確地載入和執行這個檔案。
| 格式 | 全名 | 使用平台 | 常見副檔名 |
|---|---|---|---|
| ELF | Executable and Linkable Format | UNIX / Linux | 無副檔名(可執行檔)、.so(共享函式庫) |
| PE | Portable Executable | Windows | .exe(可執行檔)、.dll(動態函式庫) |
| Mach-O | Mach Object | macOS / iOS | 無副檔名(可執行檔)、.dylib(動態函式庫) |
是的,Windows 上的 .exe 和 .dll 都是 PE 格式的檔案,只是用途不同:.exe 是可直接執行的程式,.dll 是給其他程式動態連結使用的函式庫。
以 ELF 為例,Linux 提供工具讓使用者查看 ELF 檔案的內容:
# 查看檔案類型
file main.o # 回報:ELF relocatable file
file main # 回報:ELF executable
# 詳細分析 ELF 結構
readelf main
ELF 格式中包含程式的進入點 (Entry Point),即程式開始執行的第一條指令的位址。
ELF 提供了跨 Linux 和 UNIX 系統的通用標準,但 ELF 格式本身並不綁定到特定的 CPU 架構。這意味著一個在 x86 上編譯的 ELF 可執行檔,無法直接在 ARM 處理器上執行,即使兩者都使用 ELF 格式。格式相同,並不代表可以互相執行。