一、邏輯地址轉線性地址
機器語言指令中出現的內存地址,都是邏輯地址,需要轉換成線性地址,再經過MMU(CPU中的內存管理單元)轉換成物理地址才能夠被訪問到
我們寫個最簡單的hello world程序,用gcc編譯,再反匯編后會看到以下指令:
mov 0x80495b0, %eax
這里的內存地址0x80495b0 就是一個邏輯地址,必須加上隱含的DS 數據段的基地址,才能構成線性地址。也就是說 0x80495b0 是當前任務的DS數據段內的偏移。
在x86保護模式下,段的信息(段基線性地址、長度、權限等)即段描述符占8個字節,段信息無法直接存放在段寄存器中(段寄存器只有2字節)。Intel的設計是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT內的索引值(index)。
Linux中邏輯地址等於線性地址。為什么這么說呢?因為Linux所有的段(用戶代碼段、用戶數據段、內核代碼段、內核數據段)的線性地址都是從 0x00000000 開始,長度4G,這樣 線性地址=邏輯地址+ 0x00000000,也就是說邏輯地址等於線性地址了。
這樣的情況下Linux只用到了GDT,不論是用戶任務還是內核任務,都沒有用到LDT。GDT的第12和13項段描述符是 __KERNEL_CS 和__KERNEL_DS,第14和15項段描述符是 __USER_CS 和__USER_DS。內核任務使用__KERNEL_CS 和__KERNEL_DS,所有的用戶任務共用__USER_CS 和__USER_DS,也就是說不需要給每個任務再單獨分配段描述符。內核段描述符和用戶段描述符雖然起始線性地址和長度都一樣,但DPL(描述符特權級)是不一樣的。__KERNEL_CS 和__KERNEL_DS 的DPL值為0(最高特權),__USER_CS 和__USER_DS的DPL值為3。
用gdb調試程序的時候,用info reg 顯示當前寄存器的值:
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
可以看到ds值為0x7b, 轉換成二進制為 00000000 01111011,TI字段值為0,表示使用GDT,GDT索引值為 01111,即十進制15,對應的就是GDT內的__USER_DS用戶數據段描述符。
從上面可以看到,Linux在x86的分段機制上運行,卻通過一個巧妙的方式繞開了分段(即邏輯地址=線性地址)。Linux主要以分頁的方式實現內存管理
1.--CPU的段寄存器:
在CPU中,跟段有關的CPU寄存器一共有6個:cs,ss,ds,es,fs,gs,它們保存的是段選擇符(或者叫段描述符)。而同時這六個寄存器每個都有一個對應的非編程寄存器,它們對應的非編程寄存器中保存的是段描述符。系統可以把同一個寄存器用於不同的目的,方法是先將其寄存器中的值保存到內存中,之后恢復。而在系統中最主要的是cs,ds,ss這三個寄存器。
-
CS 代碼段寄存器:指向包含程序指令的段,在CS寄存器中RPL用於表示當前CPU的特權級(CPL),CPL為0是最高權限(內核態使用),CPL為3是用戶態使用。
-
SS棧段寄存器:指向當前程序的棧的段。
-
DS 數據段寄存器:指向保存着靜態數據和全局數據的段(靜態區)。
2.--段描述符
段描述符就是保存在全局描述符表或者局部描述符表中,當某個段寄存器試圖通過自己的段選擇符獲取對於的段描述符時,會將獲取到的段描述符放到自己的非編程寄存器中,這樣就不用每次訪問段都要跑到內存中的段描述符表中獲取。
-
BASE(32位):段首地址的線性地址。
-
G:為0代表此段長度以字節為單位,為1代表此段長度以4K為單位。
-
LIMIT(20位):此最后一個地址的偏移量,也相當於長度,G=0,段大小在1~1MB,G=1,段大小為4KB~4GB。
-
S:為0表示是系統段,否則為代碼段或數據段。
-
Type:描述段的類型和存取權限。
-
DPL:描述符特權級,表示訪問這個段CPU要求的最小優先級(保存在cs寄存器的CPL特權級),當DPL為0時,只有CPL為0才能訪問,DPL為3時,CPL為0為3都可以訪問這個段。
-
P:表示此段是否被交換到磁盤,總是置為1,因為linux不會把一個段都交換到磁盤中。
-
D或B:如果段的LIMIT是32位長,則置1,如果是16位長,置0。(詳見intel手冊)
-
AVL:忽略。
2.1--數據段描述符:
表示這個段描述符代表一個數據段,這種描述符可以放在GDT或者LDT。該描述符的S標志位為1,也就是非系統段。需要注意內核數據段屬於數據段描述符,並不屬於系統段描述符。
2.2--代碼段描述符:
表示這個段描述符代表一個數據段,這種描述符可以放在GDT或者LDT。該描述符的S標志位為1,也就是非系統段。需要注意內核代碼段屬於代碼段描述符,並不屬於系統段描述符
3.--全局描述符表與局部描述符表
全局描述符表和局部描述符表保存的都是段描述符,記住要把段描述符和段選擇符區別開來,保存在寄存器中的是段選擇符,這個段選擇符會到描述符表中獲取對於的段描述符,然后將段描述符保存到對應寄存器的非編程寄存器中。
系統中每個CPU有屬於自己的一個全局描述符表(GDT),其所在內存的基地址和其大小一起保存在CPU的gdtr寄存器中。其大小為64K,一共可保存8192個段描述符,不過第一個一般都會置空,也就是能保存8191個段描述符。第一個置空的原因是防止加電后段寄存器未經初始化就進入保護模式而使用GDT。
而對於局部描述符表,CPU設定是每個進程可以創建屬於自己的局部描述符表(LDT),當前被使用的LDT的基地址和大小一起保存在ldtr寄存器中。不過大多數用戶態的liunx程序都不使用局部描述符表,所以linux內核只定義了一個缺省的LDT供大多數進程共享。描述這個局部描述符表的局部描述符表描述符保存在GDT中
4.--分段機制將邏輯地址轉化為線性地址的步驟:
1)使用段選擇符中的偏移值(段索引)在GDT或LDT表中定位相應的段描述符.(僅當一個新的段選擇符加載到段寄存器中是才需要這一步)
2)利用段選擇符檢驗段的訪問權限和范圍,以確保該段可訪問。
3)把段描述符中取到的段基地址加到偏移量(也就是上述匯編語言匯中直接出現的操作地址)上,最后形成一個線性地址。
二, 線性地址轉物理地址
邏輯地址:是相對於段而言的,需要段描述符和段內偏移來組成。所有段都從0x00000000開始,只需關注段內偏移即可。而段內偏移的值恰好等於線性地址的值。
線性地址:是進程使用的地址,虛擬的地址。人為抽象出一大片地址空間給進程使用,為了方便32位地址總線存取,linux內核定義為了4G。
物理地址:是采用32位總線存取物理內存某個字節時,地址總線上電位的高低。
分段單元將邏輯地址轉換成線性地址,分頁單元將線性地址轉換成物理地址。此處分析后者
CPU通過地址來訪問內存中的單元,地址有虛擬地址和物理地址之分,如果CPU沒有MMU(Memory Management Unit,內存管理單元),或者有MMU但沒有啟用,CPU核在取指令或訪問內存時發出的地址將直接傳到CPU芯片的外部地址引腳上,直接被內存芯片(以下稱為物理內存,以便與虛擬內存區分)接收,這稱為物理地址(Physical Address,以下簡稱PA),如下圖所示。
如果CPU啟用了MMU,CPU核發出的地址將被MMU截獲,從CPU到MMU的地址稱為虛擬地址(Virtual Address,以下簡稱VA),而MMU將這個地址翻譯成另一個地址發到CPU芯片的外部地址引腳上,也就是將虛擬地址映射成物理地址,如下圖所示
虛擬內存地址和物理內存地址的分離,給進程帶來便利性和安全性。虛擬地址必須和物理地址建立一一對應的關系,才可以正確的進行地址轉換。
記錄對應關系最簡單的辦法,就是把對應關系記錄在一張表中。為了讓翻譯速度足夠地快,這個表必須加載在內存中。不過,這種記錄方式驚人地浪費。
因此,Linux采用了分頁(paging)的方式來記錄對應關系。所謂的分頁,就是以更大尺寸的單位頁(page)來管理內存。在Linux中,通常每頁大小為4KB。如果想要獲取當前樹莓派的內存頁大小,可以使用命令:
baiheng@baiheng-OptiPlex-5055-Ryzen-CPU:~$ getconf PAGE_SIZE
4096
地址轉換過程
32bit 分頁機制下虛擬地址是由32bit組成的,常規4KB分頁,32位的虛擬地址被分成3個域。
依據以下步驟進行轉換:
- 從cr3中取出進程的頁目錄地址(操作系統負責在調度進程的時候,把這個地址裝入對應寄存器);
- 根據線性地址前十位,在數組中,找到對應的索引項,因為引入了二級管理模式,頁目錄中的項,不再是頁的地址,而是一個頁表的地址。(又引入了一個數組),頁的地址被放到頁表中去了。
- 根據線性地址的中間十位,在頁表(也是數組)中找到頁的起始地址;
- 將頁的起始地址與線性地址中最后12位相加,得到最終我們想要的葫蘆;
前面說了二級頁管理架構,不過有些CPU,還有三級,甚至四級架構,Linux為了在更高層次提供抽像,為每個CPU提供統一的界面。提供了一個四層頁管理架構,來兼容這些二級、三級、四級管理架構的CPU。這四級分別為:
- 頁全局目錄PGD(對應剛才的頁目錄)
- 頁上級目錄PUD(新引進的)
- 頁中間目錄PMD(也就新引進的)
- 頁表PT(對應剛才的頁表)。
整個轉換依據硬件轉換原理,只是多了二次數組的索引罷了,如下圖: