JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

什麼是系統程式設計?又為什麼是 C?

認識「為其他軟體打地基」這門工作,以及它底下那台其實很單純的機器。我們先勾勒整張地圖(CPU、記憶體、各個層級、編譯與執行的循環),最後真的跑一支小小的 C 程式。

開車,還是造引擎

你看過的大部分程式,目標都是某個「人」的問題:一個結帳頁面、一個聊天 app、一款遊戲。那是應用層的工作——你站在別人早就堆好的高高一疊機制之上,相信「開檔案」或「送訊息」就是會動。系統程式設計(systems programming) 則是去打造底下那堆機制。它寫的是作業系統、驅動程式、資料庫、編譯器與執行期環境——其他所有程式都安靜地站在它們上面。一句話:系統程式設計 就是「為其他軟體撐起地基」的這門手藝。

分界線在於「離機器有多遠」。應用程式 關心的是使用者眼中的意義——金額對不對、訊息有沒有送達。系統程式直接關心機器的資源:CPU 的時間、記憶體裡的位元組、磁碟上的檔案、從網路上一個個進來的位元。正因為這是它的職責,系統程式碼會非常在意硬體究竟怎麼運作,而它通常用 C、C++、Rust 這類「讓你掌控細節、而非藏起細節」的語言寫成。

這也是為什麼這份工作毫不留情。應用程式的錯誤,可能只是畫面上多算了一個數字;系統程式的錯誤,卻可能弄壞它上面的每一支程式,或讓整台機器當掉。讓系統程式碼又快又精準的那股底層力量,同時也把安全網一併拿掉——於是語言常常會讓你做錯事卻一聲不吭。「以責任換取掌控」這筆交易,正是整條學習階梯的主軸。

底下那台機器,比你想的單純

把層層外殼剝掉,一台電腦其實就是三個互相配合的部分。一顆CPU(處理器)負責思考;一塊記憶體像一長排編了號的儲存格;還有I/O——鍵盤、螢幕、磁碟、網路——負責跟外界打交道。有一個觀念你可以記一輩子:記憶體同時存放你的資料,和程式自己的指令,兩者並排躺著,都只是單純的數字。程式並不是住在某個特別地方的魔法——它就是位元組,和其他東西躺在同一塊記憶體裡。

那麼,一塊矽晶片是怎麼「跑」程式的?它不是靠理解,而是靠每秒重複數十億次同一個小儀式。CPU 裡有個特殊的指標,記著下一條指令的位址;CPU 把那裡的位元組取來、解碼出它的意思、執行那一步(把兩個數相加、複製一個值、跳到別處),然後前進到下一條指令,再重複。整顆引擎就這樣。迴圈與判斷,不過就是「會改變下一個位址」的指令而已。

高階、低階,以及「底層硬體」

想像兩種報路方式。高階版:「去圖書館。」低階版:「左轉、走 200 公尺、紅綠燈過馬路、右轉。」兩種都會到,差別在於用字離那些細小的實際動作有多近。程式語言也落在同一條尺上。低階與高階 這條軸,量的就是:你的指令離 CPU 與記憶體真正在做的事有多近,還是離人類較大的意圖有多近。

高階語言(Python、JavaScript)讓你說出「想要什麼」——排序這個串列、抓取這個網址——再由一大堆看不見的機制把它轉成機器步驟。低階語言(C,以及更底下的組合語言)則逼你用「接近硬體實際動作」的方式表達:哪些位元組放到哪、一個值到底佔幾個位元組。越低階,掌控越多、扶持越少;越高階,便利越多、安全越多。兩者沒有優劣,是不同的工作。系統程式設計住在偏低階的那一端,而跑在那裡的程式碼,就是我們說的 貼近硬體(close to the metal)

編譯還是直譯,以及那座層層相疊的高塔

要讓電腦執行你用人類友善語言寫的程式碼,基本上有兩條路。你可以先把整份原始碼翻成機器自己的語言,再去跑翻好的結果——像把一本書一次翻譯完、印出來。或者,邊跑邊一行一行翻譯與執行——像身邊站著一位即時口譯。前者叫編譯,後者叫直譯。編譯與直譯 的分野是真的存在,只是界線有時模糊(許多語言先編譯成位元組碼、再交給直譯)。C 牢牢站在編譯這一側:一支叫編譯器的程式讀你的原始碼,產出一個可獨立執行、裝著機器碼的檔案,你直接拿來跑。

為什麼這在這裡很重要?因為編譯器把可讀的文字,變成 CPU 取指—執行循環看得懂的、精確的機器碼,執行時你的程式與矽晶片之間沒有直譯器擋著。這正是 C 又快又可預期的原因——也是我們用它來學這個領域的理由。誠實地說,代價是:機器碼綁定特定的 CPU 家族與作業系統,所以你通常要替每個目標平台各自編出一份,而不是一個檔案到處都能跑。

你編好的程式到底坐在哪裡?坐在一座層層相疊的高塔裡,每一層都把下面的雜亂藏起來,向上面提供更乾淨的服務。由下而上:硬體;負責安全地把硬體分給各程式的作業系統核心(kernel);把核心原始服務包裝成友善函式的系統函式庫(例如 C 標準函式庫);最上面才是你的應用程式。這就是 抽象層疊(abstraction stack)。最上層的「印出這一行」會變成一個函式庫呼叫,再變成對核心的請求,再變成送往裝置的訊號——而你的程式碼從頭到尾看不到下面那些步驟。系統程式設計,大致就是「在多數人停下的那層之下繼續做事」的手藝。

CPU 本身在這座高塔裡畫了一條硬界線:使用者空間,你的程式在這裡權限受限地執行;核心空間,作業系統在這裡完全掌控硬體。你的程式不能直接碰硬體——它必須透過唯一一道有人看守的門去「請求」。這個穿越動作叫做系統呼叫,它並不只是一次普通的函式呼叫;它會把 CPU 切換進核心模式。我們現在只是瞥一眼這條界線;第 4 篇〈各個層級〉以及後面的「作業系統介面」這一級,會把它講精準。

你將長住的那個循環,與你的第一支程式

日常裡,寫 C 不是一次偉大的創作,而是一個你不斷反覆轉動的小循環。你編輯原始碼文字,把它編譯成可執行的東西,執行它看會發生什麼——然後幾乎總是回頭再編輯,因為它錯了或還沒寫完。這個節奏就是 編輯—編譯—執行循環,是系統開發的心跳;在 C 裡,編譯這一步自成一道關卡,許多錯誤會在程式還沒跑之前就被攔下。我們就真的來轉一次這個循環。幾乎每位程式設計師的第一支程式,都是印出「Hello, world!」就停——一支 hello world 程式,小到可以憑記憶打出來,卻足以證明整條鏈都通了。把下面這段存成 hello.c:

#include <stdio.h>

int main(void) {
    printf("Hello, world!\n");
    return 0;
}
hello.c——引入標頭以取得 printf()、定義 main()(每支 C 程式的起點)、印出文字,再回傳 0 表示成功。
  1. 編譯它。在終端機執行 gcc -Wall hello.c -o hello。其中 -Wall 要編譯器把可疑的程式碼提出警告;從第一天就把警告打開,是 C 裡最好的習慣之一。
  2. 如果它什麼都沒印,代表編譯成功——沉默就是沒有錯誤,你現在有了一個叫 hello 的可執行檔。如果它印出指著某一行的錯誤,那是編譯期錯誤:改好那一行,再編譯一次。
  3. 執行它。輸入 $ ./hello 後按下 enter。你應該會看到 Hello, world! 出現。前面的 ./ 是告訴 shell:要跑的是這個資料夾裡的程式。
  4. 看看它回報了什麼。緊接著輸入 echo $?,你應該會看到 0——那就是你的 return 0; 產生的離開狀態,是程式回給「啟動它的人」的一句話成功報告。

這短短一次執行,其實一口氣動用了整座高塔。你的文字變成機器碼(編譯器),被打包成可執行檔,被複製進記憶體並啟動(載入器),呼叫進 C 標準函式庫(printf()),由它請求核心寫到你的螢幕(一次系統呼叫),最後交回一個 shell 讀得到的狀態。這些步驟你大多沒看見——那正是抽象層疊在盡責。本級接下來會把每一層輪流撬開,到第 5 篇時,你會在徹底明白每一行牽動了什麼的情況下,重新打造這支 hello world。