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 80 24°C PM2.5 21
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 12

位址空間與 malloc

程式看到的記憶體長什麼樣:程式碼、資料、堆積、堆疊
Interaction實作 35 minute(s)

Learning Objectives

  • 從 /proc/self/maps 和教學核心的記憶體面板,認出程式碼、資料、堆積、堆疊各在哪裡
  • 說明堆疊為什麼會溢位,以及區域變數在函數結束後為什麼不能再用
  • 說明 malloc 和 brk 的關係,以及 free 之後記憶體為什麼沒有還給作業系統
  • 認出懸置指標、重複釋放、記憶體洩漏這三種錯誤,並把它們修好
  • 比較 first-fit 和 best-fit,解釋外部碎片和合併

你的程式以為自己有 128 TB 的記憶體

這支程式印出幾個東西的位址,再把作業系統記錄的「這個行程的記憶體配置」整份印出來:

main()      0x401196
"hello"     0x402004
global_var  0x404050
heap_var    0x1b3492a0
local_var   0x7ffc907584f4

/proc/self/maps:
00400000-00401000            r--p output.s
00401000-00402000            r-xp output.s
00402000-00403000            r--p output.s
00403000-00404000            r--p output.s
00404000-00405000            rw-p output.s
1b349000-1b36a000            rw-p [heap]
7a8f84200000-7a8f84228000    r--p libc.so.6
7a8f84228000-7a8f843b1000    r-xp libc.so.6
7a8f843b1000-7a8f84400000    r--p libc.so.6
7a8f84400000-7a8f84404000    r--p libc.so.6
7a8f84404000-7a8f84406000    rw-p libc.so.6
7a8f84406000-7a8f84413000    rw-p 
7a8f84499000-7a8f8449e000    rw-p 
7a8f8449e000-7a8f844a2000    r--p [vvar]
7a8f844a2000-7a8f844a4000    r--p [vvar_vclock]
7a8f844a4000-7a8f844a6000    r-xp [vdso]
7a8f844a6000-7a8f844a7000    r--p ld-linux-x86-64.so.2
7a8f844a7000-7a8f844d2000    r-xp ld-linux-x86-64.so.2
7a8f844d2000-7a8f844dc000    r--p ld-linux-x86-64.so.2
7a8f844dc000-7a8f844de000    r--p ld-linux-x86-64.so.2
7a8f844de000-7a8f844e0000    rw-p ld-linux-x86-64.so.2
7ffc90739000-7ffc9075a000    rw-p [stack]
ffffffffff600000-ffffffffff601000 --xp [vsyscall]

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

local_var 的位址開頭是 0x7ffc...,換算下來大約在 128 TB 的位置,這台機器的記憶體只有 16 GB。這些是虛擬位址:每個行程都看到一整片自己的空間,叫位址空間(address space)。怎麼對應到實體記憶體是下一課的內容,這一課先看這片空間怎麼規劃。

/proc/self/maps 每一行是一段連續的區域(真正的檔案還有位移、裝置、inode 三欄,上面的程式為了好讀沒有印):

欄位 意思
00401000-00402000 位址範圍
r-xp 權限:可讀、不可寫、可執行;最後的 p 是私有(private),s 是共用(shared)
最右邊 內容的來源:執行檔(這裡叫 output.s)、函式庫、[heap]、[stack];空白是匿名的記憶體

中間多出來的 libc.so.6、ld-linux 是 C 標準函式庫和動態連結器,它們也住在這個行程的位址空間裡。

每個區段放什麼

區段 放什麼 權限 誰決定大小
程式碼(text) 編譯好的機器碼 r-x 編譯時固定
唯讀資料(rodata) 字串常數,例如 "hello" r-- 編譯時固定
資料(data、bss) 全域變數;有初值的在 data,沒有初值的在 bss,執行時才補零 rw- 編譯時固定
堆積(heap) malloc 拿到的記憶體 rw- 執行時往高位址長
記憶體映射區 函式庫、mmap、執行緒的堆疊 看內容 執行時增減
堆疊(stack) 區域變數、函數的返回位址 rw- 執行時往低位址長

堆積和堆疊從兩頭往中間長,中間留著一大片沒有對應任何東西的位址。碰到那片空白就是 SIGSEGV。

如果你不常寫 C,這一課只會用到幾個語法:

  • &x:取變數 x 的位址。
  • int *p:p 是指標,裡面存的是一個位址;*p 是到那個位址去讀寫。
  • malloc(n):要 n 個位元組的記憶體,回傳位址;free(p):還回去。
  • printf 的 %p:把位址印成十六進位。

想多練這些語法可以到 C++ 軌。

教學核心跑的是 C 的子集,機器指令是簡化過的(load、add、store 這種,不是真的 x86)。它把位址固定下來、沒有做隨機化,每次跑都一樣;真實系統每次的堆積與堆疊位址都不同,上面印出來的值也不會和教學核心相同。

互動練習:在面板上找到每個變數

右邊切到「記憶體」面板,上半部是這個行程的區段(格式同 /proc/self/maps),下半部是有用到的頁。按「執行一步」往前走,對照程式印出的位址落在哪一段。按到底行程就結束了,記憶體會歸還,所以這題不要按「跑到結束」。

堆疊往低位址長,用完就是 SIGSEGV

每呼叫一次函數,堆疊就多一塊空間(叫堆疊框,stack frame),放這次呼叫的區域變數與返回位址;函數結束時整塊收回。這是一支遞迴三層的程式在 Linux 上的結果:

stack limit: 8388608 bytes
depth 1: local at 0x7ffcf87cf0cc
depth 2: local at 0x7ffcf87cf09c
depth 3: local at 0x7ffcf87cf06c

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

位址越來越小,所以堆疊是往低位址長。第一行是這個行程的堆疊上限:8 MB(ulimit -s 可以看,每台機器的設定可能不同)。

兩個直接的後果:

  • 函數回傳後,它的區域變數就不能用了。 回傳一個區域變數的位址(return &x;)在語法上會通過,但那塊空間下一次呼叫就被別人用掉。
  • 遞迴太深或區域陣列太大會溢位。 超過上限再往下存取,碰到的是沒有對應的位址,核心送 SIGSEGV。所以大陣列要用 malloc,不要宣告成區域變數。

互動練習:把堆疊用完

down 每一層宣告一個 4 KB 的陣列。上方統計列的「分頁錯誤」會跟著增加:堆疊是用到才配置實體記憶體的(第 14 課的需求分頁)。

malloc 不是系統呼叫

malloc 是 C 函式庫裡的函數。它一次跟核心批發一大塊,再零售給你,所以大部分呼叫根本不會進到核心:

first malloc grew the heap by 135168 bytes
second malloc grew the heap by 0 bytes
distance from a to b: 112 bytes
1 MB malloc grew the heap by 0 bytes
1 MB block is outside the heap

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

  • 第一次 malloc(100):函式庫用 brk 系統呼叫把堆積的尾端往上推了 135168 位元組,也就是 132 KB。
  • 第二次:批發來的還夠,沒有系統呼叫。
  • 兩塊相距 112 位元組,比 100 多:每塊前面有配置器自己用的標頭,大小還要對齊。
  • malloc 1 MB:堆積完全沒長,那一塊在堆積外面。依 malloc(3) 手冊,超過 MMAP_THRESHOLD(預設 128 KB)的要求改用 mmap 另外要一段,free 時可以直接還給核心。

教學核心的 malloc 一律用 brk,沒有 mmap 這條路;第一次擴充同樣是 132 KB,100 位元組的兩塊同樣相距 112。

free 只是把區塊標成可用,放回配置器的清單,下次 malloc 優先拿來用。除了 mmap 來的大區塊,記憶體通常留在行程裡,所以 top 看到的用量不會因為你 free 了就馬上下降。

三種記憶體錯誤

C 不會檢查你有沒有好好用這些記憶體,錯誤常常不會立刻爆炸:

錯誤 寫法 後果
懸置指標(dangling pointer),使用已釋放的記憶體 free(p); printf("%s", p); 未定義行為:可能讀到舊資料、別人的資料或垃圾
重複釋放(double free) free(p); free(p); 未定義行為:配置器的清單可能被破壞
記憶體洩漏(memory leak) 配置了卻再也找不到指標、也沒有 free 當下沒事;程式跑越久用越多,長時間執行的服務最後可能被核心終止

同一種寫法在真實 Linux 上的結果:

first byte after free: 119 (was 65)
same address: yes
name  = Bob
other = Bob
child killed by signal 6 (SIGABRT is 6)

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

  • free 之後第一個位元組就不是原來的 A(65)了:glibc 把剛釋放的區塊拿來記自己的清單。教學核心不會動釋放的區塊,內容會原封不動留著;兩邊都是未定義行為,不能依賴它讀到什麼。
  • 下一次 malloc 拿到同一塊,name 看到的是 other 的內容。
  • 第二次 free(name):glibc 這次偵測到了,印出 free(): double free detected in tcache 2 並用 SIGABRT 結束行程。它不是每次都偵測得到。

行程結束時,核心會收回它全部的記憶體,所以短命的小程式洩漏一點不會留下後果。要找這些錯誤可以用 Valgrind 或編譯時加 -fsanitize=address。

互動練習:看 brk 何時出現,再修好這支程式

這支程式配置三次,還有懸置指標和重複釋放。先跑一次看它怎麼死,再把它修好。「記憶體」面板的「堆積裡的區塊」可以用「執行一步」慢慢看(一步一行)。

配置器要回答的問題:放哪一個洞

程式隨意配置、隨意釋放之後,堆積會變成使用中和可用的區塊交錯。下一次 malloc(n) 要從可用區塊裡挑一個:

  • First-fit:從低位址找,第一個夠大的就用。找得快。
  • Best-fit:看過全部可用區塊,挑夠大裡面最小的。切剩的最少,但要全部看過。

切剩的小塊可能再也用不上。可用的記憶體加起來夠,卻沒有一塊連續的夠大,這叫外部碎片(external fragmentation)。C 的配置器沒辦法搬動使用中的區塊,因為你手上的指標都指著舊位址,所以只能靠挑選策略和合併(coalesce,釋放時把相鄰的可用區塊併成一塊)來減輕。

真實的配置器(例如 glibc)依大小分成很多條清單,比這兩個策略複雜得多,要解決的是同一個問題。

互動練習:first-fit、best-fit 與合併

a=10 是配置 10 KB 給 a,free a 是釋放。這個工具省略標頭與對齊,只看大小。每一塊放在哪裡都是程式依策略算出來的。

用程式寫 first-fit

這是情境一釋放之後的狀態:三個洞分別是 10、6、20。注意最後那一次:可用總量還有 20,卻找不到連續的 20。

實作:把它改成 best-fit

補完 best_fit:看過全部區塊,挑夠大裡面最小的;一樣大挑前面的。跑完的結果可以和上面「情境一」的 Best-fit 對答案:三個要求全部成功,什麼都不剩。

你的輸出
預期輸出

確認一下

1.

下面這個函數有什麼問題?

int *make(void) {
    int x = 5;
    return &x;
}
2.

一支迴圈裡呼叫 100 萬次 malloc(16) 再 free 的程式,大約會產生幾次 brk 系統呼叫?

3.

程式 free(p) 之後,再讀 *p 剛好得到原本的值。這代表什麼?

4.

可用記憶體總共 15 KB,分散在 5 KB 和 10 KB 兩塊,malloc(12 KB) 會怎樣(假設配置器不跟核心要更多)?

5.

為什麼大陣列不建議寫成區域變數 char buf[100000000];?

第 1 次作答

重點整理

  • 每個行程有自己的位址空間:程式碼與唯讀資料、全域變數、往上長的堆積、往下長的堆疊,中間是沒有對應的空白。/proc/self/maps 看得到實際的配置。
  • 堆疊框隨函數呼叫建立與收回,所以不能回傳區域變數的位址;超過上限是 SIGSEGV。
  • malloc 是函式庫函數,跟核心批發(brk,大區塊用 mmap)再零售;free 只是放回清單,不代表還給核心。
  • 懸置指標、重複釋放是未定義行為,常常不會當場出事;洩漏是越跑越多。
  • First-fit 快、Best-fit 切剩的小但要全部看過,沒有哪個永遠比較好。外部碎片靠合併和策略減輕,C 的配置器不能搬動使用中的區塊。

延伸閱讀:OSTEP(作者免費公開)的 Address Spaces、Memory API、Free Space Management 幾章。

下一課回答這一課留下的問題:這些虛擬位址怎麼變成實體位址,每次都查表不會太慢嗎?

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