操作系統-IA32的地址轉換

概述

該篇介紹的是 IA-32/Linux中的地址轉換 , 轉化的動機是什麼? 是如何轉化的 ?

下文的 段描述符描述符表 太難理解, 可以近似認爲

段描述符 = 段表項 , 描述符表 = 段表

邏輯地址 線性地址 物理地址

1297993-20220414220918358-660376470.png

邏輯地址和線性地址的的轉化如下 :

邏輯地址 --- (分段) ---> 線性地址 --- (分頁)  ---> 物理地址 

寄存器

IA-32 的寄存器
1297993-20220414221927030-189338752.png

下面很明顯是個進程, 而進程中的各種段存在在哪些段寄存器
1297993-20220414222623389-1957908778.png

段選擇符

    作用 : 指定哪一個段表 , 在段表的什麼位置的那一個段表項(段表的偏移量) 
    目的 : 程序獲取到了段選擇符最終是爲了獲取到段描述符 ,記住這一點很重要

每個進程都有一個段表
段寄存器(16位就是 CS SS DS那些) : 每個進程都有不同的段區間 ,就是不同的section , 假如進程A ,取指令的時候 ,那麼 CS 段寄存器存放的就是 進程A 代碼段的一個描述符(描述這個段信息的結構稱之爲 "段描述符") , 段描述符由三部分組成 : 權限 + 類型 + 索引 , 段描述符類型分兩種 ,一種 GDT ,一種 LDT

1297993-20220414222835152-1991620585.png

全局描述表(GDT) 和 局部描述符表(LDT) , 這兩個是段表 , 即是表明這個段描述符是來自哪個段表的!!

段表 段表項分類

我們認真想想, 按照功能分 ,肯定會有不同的段表 , 每種段表既然功能不同 ,那麼裏面的段表項就不一樣 .

(這張PPT , 上面的是段表項的分類 ,下面的是段表的分類 )

1297993-20220414224228560-1492047745.png

介紹段表

GDT整個系統只有一個 , TSS在 GDT 中, 可以看到GDT 的第16項是 TSS , 段選擇符是 0x80
1297993-20220413171114645-1904121433.png

左下角 kernel_code , kernel_data , user_code , user_data , 後面的段選擇符 ,__KERNEL_CODE (值 : 0x60) 是Linux 的宏(常量),

問題1 : 全局描述表(GDT Global Descriptor Table) 只有一個,裏面有用戶代碼段,用戶數據段以及TSS , 也就是說每個進程的代碼段都要放進 GDT 裏去嗎??

問題2 : 段表放在哪 ? 什麼時候加載進去的

段表在內存裏面 , 在操作系統啓動就得加載進去吧?

介紹段表項

通過 GDT 或是 LDT 找到段描述符 , 我們來看一下段描述符長什麼樣

1297993-20220413172439670-899485784.png

可以看到組成有 : 段基地址 + 限界 + 標誌位 , S 這個表示的是: 系統段表項還是普通段表項 , A 類似於髒頁一樣的標識 , 這裏還有一個 P ,

問題1 : 段表項 P 的作用是啥 ?

P = 0 表示這一頁在不在主存 , Linux 默認它總是爲 1 ,表示段一直在內存 ,即是 Linux 只考慮分頁 ,不考慮分段, 假如是8086 這些機器的呢? P=0 表示該段不在內存中 ,那麼得先加載段 .

段表項的緩存

換進程的時候 ,CS ,SS ,DD 這些寄存器裏的段選擇符就會給換走 , 段選擇符目的 : 程序獲取到了段選擇符最終是爲了獲取到段描述符 ,段選擇符 --> 段表 --> 段表項 那要是CPU 每次都去內存拿段表項太慢了, 所以肯定有個緩存 ,這個緩存就是寄存器 ,GDTR 會放 GDT 的首地址

img

上面PPT 的藍色字寫到換新進程的時候 , 會利用cache緩存段表項 , 還有一個點就是Linux 把描述符 cache 的基地址設置爲 0 , 表示那某個段表的第一項 ,而Linux 中 GDT 的第一項是空的,

我們再看回剛纔介紹段表的圖片 :

1297993-20220413171114645-1904121433.png

GDT 的第16項是 TSS , 段選擇符是 0x80 , 那麼寄存器 TR 裏面放的就是 0x80 .

邏輯地址如何轉化爲線性地址

下圖是是一個概括圖, 得到一個邏輯地址後分爲兩部分, 一部分爲了找到 段基址 , 另一部分是段內偏移量, 這兩部分結合形成了線性地址
1297993-20220414100128911-2027062449.png

步驟
(1) 根據段描述符中的T1 的值判斷是 GDT 還是 LDT
(2) 到 GDTR 中獲取到 GDT 段首地址 , 加上偏移量, 就獲取到了段表項 , 然後會把段表項放在 cache中去
(3) 段表項裏有基地址 , 獲取基地址加在段內偏移量 , 得到線性地址

這是第一次取的時候是這樣 , 後面就可以到 cache 中去取

下面兩張圖也是闡述這個過程

1297993-20220414221759471-988305463.png

1297993-20220414222323441-39275921.png

我們可以看到段寄存器中存放着段選擇符, 而根據選擇符又可以獲得段表項 ,段表項又可以獲得段基址, 得到段基址最終就得到了線性地址.

段寄存器 ---> 段選擇符  ----> 段表項  ---> 段基址 ---> 線性地址

Linux 的分段處理

1297993-20220415063312242-1656422922.png

1297993-20220415062820733-75514551.png

上圖是 Linux 下的分段機制, 其爲了擴展性,簡化了分段機制, RISC 對分段支持非常有限, 但是IA-32 底層的硬件又提供了分段的功能,所以只能對其簡化 ,可以看到用戶和內核相關的代碼和數據段的 基地址 全部設置爲 0 , 限界設置爲最大 ,全部爲 1 , G = 1 , P = 1 , 相當於說每一個段都佔了 4GB 的空間, 也就是說它不分段了, P = 1 ,表示它沒有使用硬件的分段機制調進調出, 而是使用分頁進行調進調出 .

    可以看出來,它們的TI爲0,表示都保存在全局段描述符表中。可能看到這裏大家會有個疑問,既然用戶段的RPL爲3,那怎麼去訪問DPL爲0的內核段呢,這就是linux精明的地方,它就是禁止用戶態訪問內核態的數據,但是內核爲用戶態開了兩個小門,然用戶態能夠通過這兩個小門進入到內核態中,這兩個小門就是系統調用與中斷和異常。

題外話

            來自參考文章 : 

            Linux與Windows的分段機制原理上類似,都是扁平式的,段基址爲0,也就是說CS,SS這些寄存器全部都是0,直接把整個虛擬內存看成一整個“段”。所以簡單來說,它們並不想使用這個從16位系統遺留下來的分段機制,而CPU爲了保持兼容性還保留了這些分段機制,所以現代OS大都使用這種扁平式的分段管理,將CPU「糊弄」過去。不過,這並不是說 Linux完全沒有利用到段寄存器。事實上,Linux在實現線程本地存儲(Thread Local Storage)的時候使用到了GS寄存器,用GS寄存器存儲了TLS的基址,這樣做的好處是加快了訪問速度。之所以可以這麼做也是因爲Intel對FS,GS這些段寄存器的管理比較鬆散,Linux就剛好用它來幹這個事兒了。

例子

img

img

線性地址向物理地址轉化

假如我們的主存是 4GB , 每頁的大小是 4KB 我們來算一下分頁模式下的頁表項的數目 , 頁表項的數目 = 主存大小 / 頁大小 = 4GB / 4KB = 1024 * 1024
我們學過java知道假如用 hashmap 來儲存這些頁表項那麼這個hashmap 也太大了吧, 所以可以進行分級 ,即 HashMap <頁表項, HashMap <頁表項, 內容> > ,即是說一級頁表(稱爲頁目錄)就有1024 項(頁目錄項), 假設每一項大小爲 4個字節(32位), 那麼頁目錄項剛好爲 4KB 一頁. 明白了這個, 我們看一下分頁地址過程

1297993-20220415070312306-1140343061.png

補充 I7 CPU 尋址過程

1297993-20220415070412638-418633409.png

MMU 那個位置就是邏輯地址進行轉換的地方

1297993-20220415164045666-2116635657.png

1297993-20220415163719697-1076747868.png

這裏 linux 的任務的表示 ,其中 mm 這個字段存放這頁表, 表示這個進程,佔用的數據, 而這些數據被映射到物理內存中去, 於是就有頁表映射,CPU 切換到那個進程, 該進程就應該把進程裏的 全局目錄地址 放置到 CR3 這個寄存器中 , 而今該寄存器纔會去找對應的數據 (見上面兩張圖) ; mmap 指向的是一個鏈表 ,同時

1297993-20220415173943125-490379189.png

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章