iT邦幫忙

2024 iThome 鐵人賽

DAY 20
0
自我挑戰組

Linux Kernel 網路巡禮系列 第 20

記憶體管理 (5) - 記憶體 Layout 與權限管理

  • 分享至 

  • xImage
  •  

在 Linux 作業系統中,程式的執行環境分為使用者模式(user mode)和核心模式(kernel mode)。當程式在使用者空間(user space)中運行時,執行的是應用程式的指令;當程式透過系統呼叫(system call)或其他中斷請求進入核心模式時,則執行的是作業系統核心的程式碼。這種模式切換的目的是為了隔離應用程式,避免其修改其他程式、操作系統的資料,或執行高權限的指令。

為了實現這種隔離,Linux 核心必須防止應用程式訪問核心的記憶體內容。一個簡單的想法是建立完全獨立的虛擬位址空間,這樣使用者模式下的程式就無法訪問核心記憶體。然而,實際上並非如此。在 Linux 上,不同程式的虛擬記憶體空間是彼此獨立的,但使用者模式與核心模式共用同一個位址空間。

32位元系統的記憶體空間配置

以 32 位元定址下的 4GB 虛擬位址空間為例,我們可以看到位址空間被分成兩個部分:

https://ithelp.ithome.com.tw/upload/images/20241004/20152703wjeOrTLyc8.png

  • 0~3GB:給 user mode 的程式使用。
  • 3GB~4GB:給 kernel 使用。

Intel 權限模型與保護機制

那 user space 的程式為什麼不能夠訪問 kernel memory 呢? 這就要靠 CPU 的硬體功能拉。

https://ithelp.ithome.com.tw/upload/images/20241004/20152703PwViTABxLL.png

在 Intel 的權限模型中,CPU的操作權限分為四個級別,從 Ring 0 到 Ring 3。Ring 0 是最高級別,通常由作業系統核心使用;Ring 3 是最低級別,用於執行一般應用程式。Linux Kernel 運作在 Ring 0,而 User Mode 則運作在 Ring 3。

當前執行中的權限等級稱為 Current Privilege Level (CPL),該數值保存在 CS 暫存器的末兩位,對應 CPU 當前所在的保護環境(Protection Ring)。

在分段機制中,每個段描述符 (Segment Descriptor) 有一個 Descriptor Privilege Level (DPL),用來表示該段應該使用的權限等級。不過,因為 Linux bypass 分段機制,所以這裡不再詳細討論。

至於分頁機制,則會透過頁表項(Page Table Entry, PTE)的第 2 位(User/Supervisor, U/S)設置記憶體頁面的訪問權限,從而防止使用者空間程式訪問核心空間的頁面。不過權限判斷機制相對複雜,所以 Intel 手冊中有一整節專門討論,這邊只簡單提到。

為了進一步保護核心記憶體,Linux 引入了內核位址空間佈局隨機化(Kernel Address Space Layout Randomization, KASLR)技術,這使得核心記憶體的線性位址並不固定。此外,為了應對 2018 年爆發的 Meltdown 漏洞,Linux 也引入了內核頁表隔離(Kernel Page Table Isolation, KPTI),將使用者空間與核心空間的頁表完全分離。這也使得,理論上來說,兩者不再共用相同的位址空間。

Kernel 與物理記憶體

大家可能會有一個疑問:對於一個正在執行的 Process,不論是在 User mode 還是 Kernel mode,它們都使用同一個虛擬位址空間。然而,虛擬位址空間的運作依賴記憶體管理單元(Memory Management Unit, MMU)、分段表與分頁表,這些資料結構是由核心在記憶體中維護的。那麼,這些資料結構是如何運作的呢?

CPU 啟動與模式切換

首先,在 CPU 剛啟動時,它運作於 Real Address Mode。在這個模式下,CPU 可以直接存取物理記憶體地址。因此,系統可以對物理記憶體進行配置。當 Linux 核心載入後,它會初始化分段表和分頁表,隨後切換到 32 位元的保護模式(Protected Mode)也會打開 Paging 功能,此時 MMU 開始運作,並且 CPU 不再直接存取物理位址。

https://ithelp.ithome.com.tw/upload/images/20241004/20152703dkG5tBoSGe.png

在 Intel CPU 上,保護模式的啟用與分頁功能的開關由 CR0 暫存器控制。可以看到,CR0 的第 31 位控制了分頁功能,而第 0 位則控制模式切換。當 Linux 核心初始化分段表和分頁表後,它會設定這兩個位元,最終使核心在虛擬位址空間中運作。

物理記憶體映射

一旦進入保護模式並啟用分頁功能後,核心將無法直接訪問物理記憶體地址。為了克服這一困難,Linux 核心會在虛擬記憶體空間中為物理記憶體建立一個直接映射區(圖中 Direct Mapping of All Physical Memory)。

https://ithelp.ithome.com.tw/upload/images/20241004/20152703J55vwlE6Z2.png

透過分頁表的設計,核心可以將對該區域的讀寫操作直接映射到物理記憶體,從而實現物理記憶體的管理。

上圖以 4-level page table 為例,直接映射區的大小為 64TB,因此完全不必擔心 RAM 過多而導致映射區無法對應所有的實體記憶體。

https://ithelp.ithome.com.tw/upload/images/20241004/20152703myFRy22FrG.png

地址轉換函數

inux 核心提供了一些函數來進行虛擬地址與物理地址之間的轉換,例如 __va__pa。其中,__va 可以將一個物理地址轉換為虛擬地址,而 __pa 則可以將虛擬地址轉換回對應的物理地址。

// arch/x86/include/asm/page.h
#define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))


#define __pa(x)		__phys_addr((unsigned long)(x))
// arch/x86/include/asm/page_64.h
#define __phys_addr(x)		__phys_addr_nodebug(x)
static __always_inline unsigned long __phys_addr_nodebug(unsigned long x)
{
	unsigned long y = x - __START_KERNEL_map;

	/* use the carry flag to determine if x was < __START_KERNEL_map */
	x = y + ((x > y) ? phys_base : (__START_KERNEL_map - PAGE_OFFSET));

	return x;
}
// x86/include/asm/page_64_types.h
#define __START_KERNEL_map	_AC(0xffffffff80000000, UL)


__va 可以把一個物理地址轉換成虛擬地址。可以發現其實現非常簡單,就是加上一個 PAGE_OFFSET,PAGE_OFFSET 代表的就是直接映射區的起始位置,所以就可以找到一個物理記憶體地址,在虛擬記憶體中映射區的虛擬地址。

y = x - __START_KERNEL_map
x < y => x = x - PAGE_OFFSET
x > y => x = phys_base + (x - __START_KERNEL_map)

__pa 做的事情則是給定一個虛擬地址,給出對應的物理地址。不過 __pa 只是一個簡單的 macro 他無法計算任意 Paging 過的記憶體,他只能計算直接映射區的記憶體。

從程式碼中會看到他的計算過程有點複雜,這邊我們把它拆成兩種情況看:

  1. x < y: 假設輸入地址在前面介紹的直接映射區,所以只要減掉 PAGE_OFFSET即可
  2. x > y: 可以看到他是去檢查,輸入 x 是不是在 __START_KERNEL_map 之後,這個對應到前面 64 bit memory layout 圖中最下面的區塊,kernel text mapping ,也就是 kernel 的程式碼所在的位置。因為 linux 開機後會把 kernel code 搬到物理地址為 0,的地方,所以如果給定 kernel code的虛擬地址,只要減掉 kernel code的起始地址(虛擬)就可以知道他在物理記憶體的位置了。這邊會再加上 phys_base,是因為前面介紹到的KASLR (內核位址空間佈局隨機化)機制,所以會對 kernel code 的虛擬地址產生位移。

直接映射的應用

在 Direct Memory Access (DMA) 機制中,外部設備可以直接與物理記憶體進行資料傳輸,而不經過 CPU 的干預。因此,外部設備需要物理地址,而非虛擬地址。Linux 核心會在直接映射區中分配一塊記憶體給 DMA 使用,然後將該虛擬地址減去映射區的起始位置,計算出對應的物理地址,並交給外部設備進行資料傳輸。

記憶體管理 參考資料

Address Space — The Linux Kernel documentation (linux-kernel-labs.github.io)
kernel.org/doc/Documentation/x86/x86_64/mm.txt
dibingfa/flash-linux0.11-talk: 你管这破玩意叫操作系统源码 — 像小说一样品读 Linux 0.11 核心代码 (github.com)
Memory mapping — The Linux Kernel documentation (linux-kernel-labs.github.io)
Segmentation in Intel x64(IA-32e) architecture - explained using Linux (nixhacker.com)
Intel® 64 and IA-32 Architectures Software Developer Manuals


上一篇
記憶體管理 (4) - 略過 Segmentation
下一篇
PCI 及 PCI-E 簡介
系列文
Linux Kernel 網路巡禮30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言