National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 72 24°C PM2.5 13
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 01

作業系統與系統呼叫

程式怎麼跟作業系統要東西:核心模式、使用者模式、系統呼叫
Interaction 25 minute(s)

Learning Objectives

  • 說明為什麼一般程式不能直接操作硬體,以及使用者模式與核心模式的差別
  • 區分系統呼叫、硬體中斷與例外這三種進入核心的路
  • 看懂教學核心的畫面,並說出它和真實 Linux 不一樣的地方
  • 判斷 printf 的內容什麼時候才真的透過 write 送出去

一行指令就能讓程式死掉,電腦卻沒事

下面這支程式讓子行程執行 x86 的 hlt 指令,這個指令的意思是「讓 CPU 停下來」。如果任何程式都能下這個指令,一支寫錯的程式就可以讓整台電腦停擺。放到真正的 Linux 上跑:

子行程被訊號 11 終止
父行程和這台電腦都還好好的

在真正的 Linux 上實際執行的結果(Compiler Explorer 的 Linux x86-64 環境,gcc 13.3,2026-11-04)。

CPU 拒絕執行,核心把這個行程終止,其他程式照常運作。(訊號和 fork 後面的課會細講,這裡先看結果。)

這個結果背後是作業系統最基本的安排:一般程式不能直接碰硬體。原因有三個:

  • 保護:程式有錯或有惡意時,不能影響別人。你的程式不能讀走另一個程式的記憶體,也不能把整顆硬碟寫壞。
  • 共用:CPU、記憶體、磁碟只有一份,卻有很多程式同時要用,需要有人分配。
  • 抽象:程式只要說「把這些位元組寫進這個檔案」,不用知道這顆是 SSD 還是硬碟、是哪一家的產品。

負責這些事的程式叫核心(kernel)。它也是一支程式,只是有硬體賦予的特殊權限。

使用者模式、核心模式與系統呼叫

CPU 內部記著自己目前在哪一種模式:

模式 誰在跑 能做什麼
使用者模式(user mode) 你的程式、瀏覽器、Shell 計算、讀寫自己的記憶體;碰到硬體或特權指令就會被 CPU 擋下
核心模式(kernel mode) 核心 全部指令都能執行,可以操作硬體、修改任何記憶體

程式想開檔、印字、建立行程,都要請核心代勞。請託的方式叫系統呼叫(system call):程式把「要做什麼」放進暫存器,執行一個特殊指令(x86-64 上就叫 syscall),CPU 切到核心模式,跳到核心事先設定好的入口;核心檢查參數與權限,做完之後切回使用者模式,把結果交還給程式。

入口位址由核心決定,程式不能自己選要跳到核心的哪一行,所以系統呼叫是使用者程式進入核心唯一由自己發起的門。

除了系統呼叫,還有兩條路會讓 CPU 進入核心:

進入核心的路 誰引發 例子
系統呼叫 程式主動要求 read、write、fork
例外(exception) 程式執行某個指令時,CPU 自己發現問題 除以 0、存取不該碰的記憶體、在使用者模式下特權指令
硬體中斷(interrupt) 程式以外的硬體 按下鍵盤、網路封包到了、計時器響了

計時器中斷特別重要:就算程式陷入無窮迴圈,核心也會定期拿回控制權,這是多工能夠運作的基礎(第 5 課)。

C 語言的 read、write 是函式庫包的薄薄一層,幫你準備參數再執行 syscall 指令。一般程式不會直接寫那個指令,都透過函式庫呼叫。

互動練習:一個動作經過哪些層

上面的按鈕是平常會做的動作。選一個動作,按「下一步」,看它從使用者程式往下經過系統呼叫、核心、硬體,再回到程式。圖旁的彩色標籤顯示 CPU 目前在哪一種模式,步驟列裡寫著「系統呼叫」的格子就是一次系統呼叫。

這一軌怎麼讀 C

作業系統的介面是用 C 定義的,所以範例用 C 寫。不熟 C 沒有關係,這一軌用到的語法很少,先看這幾個:

#include <stdio.h>             // 使用 printf 這類函數,要先引入宣告

int counter = 0;               // 全域變數,整個程式都看得到

int main(void) {               // 程式從 main 開始
    int x = 5;                 // 區域變數
    printf("x = %d\n", x);     // %d 會換成整數 x,\n 是換行
    int *p = &x;               // &x 是 x 的位址;p 是指標,存著那個位址
    *p = 7;                    // *p 是「p 指到的那個位置」,所以 x 變成 7
    char buf[64];              // 64 個位元組的陣列
    return 0;                  // main 的回傳值就是程式的結束碼,0 代表正常
}

三個會一直遇到的習慣:

  • 指標存的是記憶體位址。系統呼叫常要你給一個位址,讓核心把結果放進去,例如 read(fd, buf, 64) 就是「把讀到的資料放到 buf 那裡」。
  • NULL 是不指向任何地方的指標。
  • 系統呼叫失敗時多半回傳 -1,原因放在全域變數 errno。真正的程式都要檢查回傳值;範例為了聚焦觀念,有時會省略。

想多了解 C 與指標,可以看 C++ 軌,指標的語法兩者相同。

教學核心是什麼,哪裡簡化了

這一軌的主角是一個在你的瀏覽器裡執行的教學核心:你寫的 C 程式會被編譯成簡化的機器指令,放到這個小核心上跑。它的行程、頁表、檔案描述子都是真的資料結構,畫面上的數字是它算出來的,不是事先做好的動畫。你也可以自己當排程器,決定下一步輪到誰。

它的目的是讓你看得見過程,所以有些地方和真實的 Linux 不同,讀的時候要記得:

項目 教學核心 真實的 Linux
機器指令 簡化成 load、add、store、call、ret x86-64 或 ARM 的真實指令
pid 固定從 1000 開始 由核心分配,每次不同
記憶體位址 每次一樣 有位址隨機化,堆積與堆疊每次不同
C 語言 int、long、char、指標、陣列、函數 完整的 C;這裡沒有 struct、浮點數、switch
時間 虛擬時鐘,sleep(2) 不用真的等 真實時間
記憶體順序 每個讀寫都照程式順序發生 多核心 CPU 可能重排,第 9 課會談
指令 /bin/ 裡只有十個小程式:echo、cat、wc、grep、sleep、true、false、yes、ls、head 完整的系統

輸出和真實 Linux 比對過,每一課也會放幾段真實 Linux 跑出來的結果,讓你知道核心之外的真實樣子。教學核心印出的 pid 和位址只在這裡有效,不要當成真實系統的值。

操作方式:

  • 編譯並開始:把程式載入核心。
  • 執行一步:走一個機器指令或一行程式(看「一步」的選項)。卡片上會寫著「下一步」要做什麼,以及這個行程目前的狀態。
  • 跑到結束:一路跑完。
  • 右邊的面板可以切換:系統呼叫列出每一次進入核心,變數與鎖顯示記憶體裡的值,記憶體是位址空間與頁表。

互動練習:看系統呼叫什麼時候出現

getpid() 向核心查詢「我的行程編號」。sleep(2) 請核心讓這個行程睡 2 秒。注意第一個 printf 的字串沒有換行。一步一步走的時候,同時看右邊的「系統呼叫」面板和終端機。

printf 不等於 write

剛才看到的就是 stdio 緩衝:C 標準函式庫在每個行程裡有一塊記憶體,printf 先把字放在那裡,累積到一定條件才一次呼叫 write。系統呼叫要切換模式,一個字呼叫一次太花時間,所以函式庫幫你累積。

什麼時候送出,要看 stdout 接到哪裡:

stdout 接到 緩衝方式 什麼時候呼叫 write
終端機 行緩衝 遇到換行、緩衝區滿了、fflush、程式正常結束
檔案或管線 全緩衝 緩衝區滿了、fflush、程式正常結束

stderr(用 fprintf(stderr, …) 輸出)預設不緩衝,因為錯誤訊息應該馬上看到。

write 不經過這個緩衝區,呼叫就直接進核心。所以 printf 和 write 混用時,順序可能和程式寫的不一樣。下面這支程式放到真正的 Linux 上跑,stdout 接到管線:

printf("1 printf\n");
write(1, "2 write\n", 8);    // 1 是 stdout 的檔案描述子,8 是位元組數
printf("3 printf\n");
2 write
1 printf
3 printf

在真正的 Linux 上實際執行的結果(Compiler Explorer 的 Linux x86-64 環境,gcc 13.3,2026-11-04)。

第 2 行先出現了,因為 1 和 3 都還在緩衝區,等到程式結束才一起送出。同一支程式在終端機上跑,順序就是 1、2、3。這也是為什麼程式在終端機看起來正常,輸出導到檔案之後順序卻變了。

互動練習:輸出接到管線時,修好順序

這個練習裡,stdout 接到管線而不是終端機(就像執行 ./main | cat),所以是全緩衝,和上面的真實輸出一樣。先跑一次看系統呼叫面板,再想辦法讓順序變成 1、2、3。

真實的 Linux 長什麼樣子

同樣用 getpid 和系統提供的資訊,在真正的 Linux 上查一次。uname 回報核心版本,sysconf(_SC_PAGESIZE) 回報記憶體分頁大小,/proc/self/maps 是核心提供的虛擬檔案,內容是這個行程的位址空間。這個檔案每一行是位址範圍、權限、位移、裝置、inode、名稱,為了容易閱讀,程式只印出起始位址、權限和名稱:

pid       = 2
kernel    = Linux 7.0.0-1014-aws x86_64
page size = 4096 bytes
--- /proc/self/maps ---
00400000     r--p output.s
00401000     r-xp output.s
00402000     r--p output.s
00403000     r--p output.s
00404000     rw-p output.s
03e8a000     rw-p [heap]
704652e00000 r--p libc.so.6
704652e28000 r-xp libc.so.6
704652fb1000 r--p libc.so.6
704653000000 r--p libc.so.6
704653004000 rw-p libc.so.6
704653006000 rw-p 
7046531d5000 rw-p 
7046531da000 r--p [vvar]
7046531de000 r--p [vvar_vclock]
7046531e0000 r-xp [vdso]
7046531e2000 r--p ld-linux-x86-64.so.2
7046531e3000 r-xp ld-linux-x86-64.so.2
70465320e000 r--p ld-linux-x86-64.so.2
704653218000 r--p ld-linux-x86-64.so.2
70465321a000 rw-p ld-linux-x86-64.so.2
7ffe9d132000 rw-p [stack]
ffffffffff600000 --xp [vsyscall]

在真正的 Linux 上實際執行的結果(Compiler Explorer 的 Linux x86-64 環境,gcc 13.3,2026-11-04)。

幾個可以對照的地方:

  • pid 是 2:這個環境跑在容器裡,看到的行程編號是獨立的一組(第 18 課)。一般的 Linux 上會是比較大的數字。
  • 每一行是一段位址,權限欄的 r、w、x 是能不能讀、寫、執行:r-xp 是程式碼,rw-p 是可以寫的資料。可以看到 [heap] 和 [stack]。output.s 是這個環境編譯出來的執行檔。教學核心的「記憶體」面板列的是同樣的資訊,第 12 課會逐段細看。
  • libc.so.6 是 C 函式庫,printf 就在裡面。
  • [vdso] 是核心放進每個行程的一小段程式。查時間這類很頻繁的呼叫可以直接在使用者模式完成,省下切換模式的成本。

位址隨機化也可以直接看到。同一支程式連跑三次,印出全域變數、堆積、堆疊的位址:

第 1 次:global 0x404020  heap 0x3e0172a0  stack 0x7fffbd774fa4
第 2 次:global 0x404020  heap 0x3a6962a0  stack 0x7fffd7f46de4
第 3 次:global 0x404020  heap 0x305f72a0  stack 0x7ffc6ca07df4

在真正的 Linux 上實際執行的結果(Compiler Explorer 的 Linux x86-64 環境,gcc 13.3,2026-11-04)。

堆積和堆疊每次都不同,讓攻擊者比較難猜到位址。這個環境編譯出的全域變數位址固定,一般發行版預設的編譯方式連程式本身的位址也會隨機。教學核心沒有這個機制,所以每次跑結果都一樣,方便你對照。

在自己的 Linux 或 WSL 上,strace ./程式 可以列出這支程式做的每一個系統呼叫,和練習裡的面板是同一個概念,只是真實程式啟動時載入函式庫也會有很多呼叫。

確認一下

1.

程式在使用者模式下執行 hlt 這類特權指令,會發生什麼事?

2.

下面哪一個函數通常不需要進入核心就能完成?

3.

在終端機上執行這段程式,這 60 秒內畫面會看到什麼?

printf("a");
sleep(60);
printf("b\n");
4.

執行 ./main > out.txt(stdout 接到檔案),out.txt 的內容順序是什麼?

printf("1\n");
write(1, "2\n", 2);
printf("3\n");
5.

下面哪一件事的來源是硬體中斷?

第 1 次作答

重點整理

  • CPU 有使用者模式與核心模式。一般程式跑在使用者模式,碰硬體或執行特權指令會被擋下。
  • 進入核心有三條路:系統呼叫(程式主動)、例外(指令出問題)、硬體中斷(外部硬體)。程式能主動走的只有系統呼叫,入口由核心決定。
  • printf 是函式庫函數,先放進行程自己的緩衝區;write 才是系統呼叫。接到終端機是行緩衝,接到檔案或管線是全緩衝。
  • 教學核心用真的資料結構執行你的程式,但指令、位址、pid、時間都簡化過;真實系統的值要看真實輸出。

下一課看一支程式跑起來之後,核心用什麼資料記住它:行程、PCB 與狀態。

這一課完成了!
Next lesson:行程與狀態
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。