接上一篇 GDT 與保護模式,這一篇將是 loader 的重點。首先咱們須要創建 kernel 空間的虛擬內存。若是你對虛擬內存的原理還不熟悉,請務必先自學,這裏能夠提供一個文檔供參考。html
到目前爲止咱們始終在物理內存上操做,確切地說是在 1MB
的低地址空間內操做,這一切都很簡單直接。可是接下來 loader 即將爲加載 kernel 作準備,咱們須要在更廣闊的 4GB
虛擬內存空間上規劃數據和代碼。java
仿照 Linux 系統,咱們將使用 3GB
以上的高地址空間做爲內核空間來開展後續全部工做。例如最基本的,目前的物理低地址 1MB 會被映射到 virtual 地址 0 ~ 1MB
以及 3GB 以上空間 0xC0000000 ~ (0xC0000000 + 1MB)
處:git
進入 kernel 之後,對低 1MB 空間的訪問將會使用 0xC0000000 ~ (0xC0000000 + 1MB)
虛擬地址,這裏主要包括當前使用的 stack,以及顯示器對應的內存映射:web
因此 video 內存基地址將從 virtual 地址 0xC00B8000
開始,不過目前沒必要深究,後續將會在顯示與打印一篇中詳解。shell
除了最基本的低 1MB 內存空間,loader 還須要進一步在 0xC0000000
以上的 virtual 空間中開疆拓土,這主要包括兩部分:segmentfault
page directory
)和頁表(page table
);下面給出整個 loader 階段將要搭建的 virtual-to-physical
內存映射關係圖:多線程
這張圖是本篇最重要的全局圖,其中第二行是第一行通過「扭曲」比例的圖示,咱們將 3GB 如下的用戶空間縮小顯示,當前重點只關注 3GB 以上的內核空間(粗框部分)。因爲是 virtual 地址空間,咱們的空間劃分能夠比較隨意和「奢侈」,咱們以 4MB 爲單位,從 0xC0000000
開始在 virtual 空間切割劃分出如下幾個區域:框架
page tables
;0xC0800000
開始,做爲加載、存放 kernel
代碼和數據的空間,也就是說 kernel
從該處開始編址;這裏要說一句,實現一個 OS 並無固定的方式,以上只是我我的的實現方式。實際上對於內存的規劃是很靈活的,就像這個項目的名字 scroll
同樣,內存就是一幅畫卷,CPU 則是畫筆,在遵循必定規則的前提下,能夠作自由發揮。ide
下面咱們首先開始橙色部分,即內核 page directory
和 page tables
的創建。函數
在開始這一段以前,咱們仍是回顧一下頁目錄(page directory
)和頁表(page table
)的相關原理。
有一些關鍵數字須要記住:
page
)的大小爲 4096;pde (page directory entry)
和頁表項 pte (page table entry)
,本質上是同樣的結構,大小爲 4 bytes
;page direcotry
一共有 1024 項,指向總共 1024 張 page table
,一共 4MB
;page table
都有 1024 項,指向 1024 張 pages
,管理着 1024 * 4KB = 4MB
的 virtual 空間;pde
管理着 4MB
的 virtual 空間;好了,下面咱們開始創建 kernel 空間的頁表。按照慣例給出代碼連接:這一部分相關的代碼從函數 setup_page 開始,供你參考。
從這裏開始如下,按照術語慣例,virtual 頁我將用 page
表述,而 physical 頁將用 frame
來表述。
首先咱們須要拿出一個 frame
,用來做爲 page directory
。回到 physical 內存分佈的那張圖,目前 1MB 如下的部分已被佔用,咱們可使用的部分就從 1MB 即 0x100000
開始。
我選擇的是 0x100000 + 4KB
,即 0x100000
後的第 2 個 frame 做爲 page directory
,固然這徹底是我的選擇;0x100000
後的第 1 個 frame 我選擇將它做爲第一個 page table
:
再次強調,這是個人我的選擇;frame 的選擇是很是自由的,只要是還沒被佔用的均可以使用,固然了你要記住本身用過了哪些 frames,合理緊湊而且儘可能「美觀」地規劃使用。
值得注意的是,第 0 和第 768 個 pde
都指向了同一個 page table
,這個 page table 咱們將用它映射 0 ~ 1MB
低內存,即咱們目前所處的 1MB 內存空間。固然這個 page table 能夠管理 4MB 的空間,咱們只映射了其中的 1MB,剩餘 3MB 的 virtual 空間就閒置了,不過這沒有關係,閒置就閒置,反正這是 virtual 空間。
下圖展現了低 1MB 內存在頁表中被映射的方式:
pde[0]
管理的是 virtual 空間最低的 4MB,其中的起始 1MB,被映射到了 physical 的低 1MB 上,這是一一對應的映射,virtual 地址徹底等於 physical 地址,這樣在打開 paging 以後,咱們對 1MB 低內存的訪問變爲使用 virtual 地址,和以前的 physical 地址訪問同樣,不會感知到任何變化。
pde[768]
管理的是 0xC0000000
即 3GB 開始的第一個 4MB 空間,回到本篇開始的第一張圖,其起始的 1MB 也被映射到低 1MB 內存上。在打開 paging 並進入 kernel 後,咱們將使用 0xC0000000 ~0xC0000000 + 1MB
的空間訪問低 1MB 內存:
這裏是本節的重點和難點。咱們知道 page directory
和 page tables
所指向的都是 physical 頁,而一旦打開了 paging 模式,咱們之後全部對內存的訪問將所有經過 virtual 地址,沒法再直接操做 physical 地址。那麼問題來了,咱們如何訪問並修改 page directory
和 page tables
自己呢?
一種方法固然是在須要時關閉 paging,直接訪問 physical 地址,以前推薦的教程 JamesM's kernel development tutorials 在不少地方都是這麼作的,不過這並非一種好的作法,緣由有如下幾點:
一個更合理的作法是,咱們將 page directory
和 page tables
自己也映射到 virtual 空間,這樣就能夠像訪問其餘正常內存同樣訪問它們。從本質上說 page directory
和 page tables
無非也是一些 page,徹底能夠和其它內存訪問一視同仁。問題就是,應該如何創建這種映射?來看下圖:
咱們將 pde[769]
指向了 page directory
這個 frame 自己。這樣 page direcotry
實際上同時也充當了一個 page table
,它所管理的正好是 1024 張 page tables 自己,一共 4MB。這 1024 張 page tables,其中有一張就是 page direcotry
它本身。
是否是有點繞?換言之,因爲 pde[769]
指向了 page directory
它本身,所以 0xC0400000 ~ 0xC0800000
這 4MB 的 virtual 空間,如今被映射到了 1024 張 page tables
上,並且更好的是,它們的 virtual 地址是徹底連續地,緊密地排布在這 4MB 空間裏。
由此,上面的問題已經解決,page tables 對應的 virtual 地址空間爲:
0xC0400000 ~ 0xC0800000
這是 4GB 空間中第 769
個 4MB 空間 (總共 1024 個 4MB 空間,組成 4GB)。
而且咱們同時還獲得了 page directory
它本身的 virtual 地址爲:
0xC0701000
即 0xC0400000 ~ 0xC0800000
這 4MB 空間中的第 769
個 page,是否是很巧妙:)
這裏的核心思想是,page directory
其實本質上是一個特殊的 page table
,它和其它 page table
同樣,都管理着 4MB 的空間。
若是感受仍是有點繞的話,你不妨反過來驗證一下,從上面給出的 virtual 地址開始,推導實際指向的 physical 地址是哪裏,我想很快就能理清這裏面的邏輯。
若是你進一步思考的話,就會發現這並非惟一的實現方式。你徹底能夠不選擇 pde[769]
,而使用其它 virtual 空間來映射 page tables,例如用 pde[770]
也能夠,這樣全部 page tables 對應的 virtual 空間就變成了 0xC0800000 ~ 0xC0C00000
。用 pde[769]
只是我我的的選擇,由於它是 0xC0000000
後的第二個 4MB 空間,這樣的安排,virtual 空間的使用能比較緊湊整齊一點。
到目前爲止,pde 768 和 769 已經被使用,即 0xC0000000 ~ 0xC0400000
和 0xC0400000 ~ 0xC0800000
這兩塊 4MB 空間已被徵用。剩下的 pde[770] ~ pde[1023]
對應的 254 個 page tables
,咱們依次爲它們安排上 frames。這樣咱們最終徵用了 256 個 pages & frames,總共 1MB 的內存(virtual & physical),來創建 kernel 空間(3GB ~ 4GB)的 page tables
,管理這 1GB 的空間。
咱們將本章開始的那個 virtual-to-physical
內存映射關係圖中的橙色部分抽出放大,展現 kernel 的 256 張 page tables 的內存分佈:
注意到咱們只分配了 kernel 空間即 3GB 以上的 page tables,共 256 張,佔地 1MB,它們映射的也是 0xC0400000 ~ 0xC0800000
空間的後 1/4 部分即 0xC0700000 ~ 0xC0800000
;而 3GB 如下的用戶空間此時並無分配 page tables,由於目前咱們並無使用到。
這 256 張 kernel 頁表(其中有一張是 page directory
自己),是咱們編寫 kernel 期間最核心的 page tables,而且在 page directory 裏創建了 pde[768] ~ pde[1023]
這所有的 256 個表項,指向了這些 page tables。
其實除了前兩個 page table,後面 254 個目前都是空的,沒有被用到,咱們只是爲它們安排好了 frame 而已。這裏用去了足足 1MB 的 physical 內存,這看上有點奢侈了,畢竟這個項目配置裏 physical 內存總共只有 32 MB(見 bochsrc.txt
,固然如今的計算機內存遠不止 32 MB,這已經不是個問題)。這樣作有一個很是重要的緣由,那就是這 256 張 kernel page tables 後面將被全部的進程(process
)共享,也就是說對於用戶 process 而言,3GB 如下的空間是隔離的,而 3GB 以上的 kernel 的空間是共享的,這也是理所固然的,不然就有多個 kernel 在內存中獨立運行了。
每次 fork
出一個新的 process,它的 page directory
的後 1/4 即 768 ~ 1023 項將會直接複製 kernel 的 page directory
的 768 ~ 1023 項,共同指向這 256 張 kernel page tables
。因此咱們要求這 256 張 page tables
對應的 frames
從一開始就固定下來,後面也再也不變化,這樣才能實現全部 process 共享的效果。
page tables
都準備就緒之後,就能夠打開 paging
了:
enable_page: sgdt [gdt_ptr] ; move the video segment to > 0xC0000000 mov ebx, [gdt_ptr + 2] or dword [ebx + 0x18 + 4], 0xC0000000 ; move gdt to > 0xC0000000 add dword [gdt_ptr + 2], 0xC0000000 ; move stack to > 0xC0000000 mov eax, [esp] add esp, 0xc0000000 mov [esp], eax ; set page directory address to cr3 register mov eax, PAGE_DIR_PHYSICAL_ADDR mov cr3, eax ; enable paging on cr0 register mov eax, cr0 or eax, 0x80000000 mov cr0, eax
這裏最重要的就是設置 CR3
寄存器,使之指向 page directory
的 frame (注意是 physical 地址),而後打開 CR0
寄存器上的 paging 比特位開關。
至此,loader 階段關於 kernel 虛擬內存初始化的部分就結束了。這一段的代碼並不長,核心僅僅是 setup_page 這一個函數,可是其背後的原理倒是很是深入複雜。在 loader 階段初步創建起 virtual memory
的框架,這對後面進入 kernel 以後的內存管理打下了良好的基礎。
在當前階段咱們全部的 virtual-to-physical 的內存分配和映射都是提早規劃,預先分配再使用的,每一塊 physical frame 都是手動安排。這其實並無徹底發揮出 virtual memory
的做用。在後面進入 kernel 以後,咱們將進一步完善 virtual memory
相關的工做,這將包括缺頁異常 (page fault)
的處理,進程 page directory
的複製等。
virtual memory
的處理是貫穿 kernel 實現和運行的底層核心工做,必須保證絕對的正確和穩定。一旦出錯,系統會馬上出現各類難以預知的奇怪錯誤甚至崩潰,而且 debug 很是困難。
下一篇咱們將會加載真正的 kernel
到內存而且轉到 kernel 開始執行代碼,這將是進入 kernel 前的最後一道關卡。