1. 程式人生 > >【自制作業系統04】從真實模式到保護模式

【自制作業系統04】從真實模式到保護模式

通過前三章的努力,我們成功將控制權轉交給了 loader.asm 這個程式。具體說就是 bios 通過載入並跳轉到 0x7c00(IMB大叔們定的) 把控制權轉交給了我們作業系統的第一個彙編程式 mbr.asm,然後 mbr.asm 裡做的事就是通過載入 loader 程式並跳轉到 0x900(這個是我們自己定的)把控制權轉交給了 loader.asm 程式,目前這個程式裡還只是向螢幕輸出一行字串“loader”,今天我們就將擴充套件它。並且今天我們要做的事,是作業系統中的第一個精彩之處,就是從真實模式跨越到保護模式。

一、真實模式與保護模式鳥瞰

我這人喜歡直面問題,其實本章只需要搞明白三個主要問題就行了,什麼是真實模式和保護模式,真實模式與保護模式的區別是什麼,怎麼進入保護模式。我先來簡單闡述下這三個問題

什麼是真實模式和保護模式

Intel 8086 是一個由 Intel 於 1978 年所設計的 16 位微處理器晶片,是 x86 架構的鼻祖。緊接著 Intel 又推出了第一款 32 位的 cpu Intel 80286(很快被淘汰,80386更經典一些),這款 cpu 由於和之前有很多不同的“保護”特性,所以稱為保護模式,也是與此同時,之前的 8086 這個 16 位 cpu 才有了真實模式的叫法。

所以什麼是真實模式和保護模式,其實就是 Intel 給自己的處理器特性命的一個名字而已,具體有哪些特性那就是細節問題了,但最起碼有一點剛剛已經有所透露,那就是保護模式至少是 32 位的,而真實模式是 16 位的(即使一個 32 位的 cpu 也有真實模式)

真實模式與保護模式的區別是什麼

  1. 真實模式 16 位,保護模式 32 位
  2. 真實模式下的地址是段暫存器地址偏移4位+偏移地址得到實體地址。保護模式下段暫存器存入了段選擇子,在段描述符表中尋找段基址,再加上偏移地址得到實體地址(開啟分頁下為邏輯地址)
  3. 這個我覺得是個 1 的推論,就是真實模式定址空間是 1M,保護模式是 4G
  4. 這個我覺得是 2 的推論,就是段描述符表記錄了段的許可權,改變了真實模式下可以隨意訪問所有記憶體的隱患(這也是保護這兩個字的體現)

怎麼進入保護模式

進入保護模式有三步:

  1. 開啟 A20
  2. 載入 gdt
  3. 將 cr0 的 pe 位置 1

可以看出進入保護模式的操作是很簡單的,但提前要做好準備工作,最重要的就是 gdt(Global Descriptor Table 全域性描述表)的準備。

二、程式碼鳥瞰

loader.asm

section loader vstart=0x900

jmp protect_mode

gdt:
;0描述符
    dd  0x00000000
    dd  0x00000000
;1描述符(4GB程式碼段描述符)
    dd  0x0000ffff
    dd  0x00cf9800
;2描述符(4GB資料段描述符)
    dd  0x0000ffff
    dd  0x00cf9200
;3描述符(28Kb的視訊段描述符)
    dd  0x80000007
    dd  0x00c0900b

lgdt_value:
    dw $-gdt-1  ;高16位表示表的最後一個位元組的偏移(表的大小-1) 
    dd gdt      ;低32位表示起始位置(GDT的實體地址)

SELECTOR_CODE   equ 0x0001<<3
SELECTOR_DATA   equ 0x0002<<3
SELECTOR_VIDEO  equ 0x0003<<3

protect_mode:
;進入32位
    lgdt [lgdt_value]
    in al,0x92
    or al,0000_0010b
    out 0x92,al
    cli
    mov eax,cr0
    or eax,1
    mov cr0,eax
    
    jmp dword SELECTOR_CODE:main
    
[bits 32]
;正式進入32位
main:
mov ax,SELECTOR_DATA
mov ds,ax
mov es,ax
mov ss,ax
mov esp,LOADER_STACK_TOP
mov ax,SELECTOR_VIDEO
mov gs,ax

mov byte [gs:0xa0],'3'
mov byte [gs:0xa2],'2'
mov byte [gs:0xa4],'m'
mov byte [gs:0xa6],'o'
mov byte [gs:0xa8],'d'

jmp $

這裡說說我的心得體會,現在看整段的程式碼雖不能說每一行讓我自己寫能寫出來,但現在看起來極為清晰。我現在其實已經想不起來當時為什麼理解了好久好久就是理解不了,除錯了好半天也老是有各種問題。不過這個程式碼是我去掉了一些可有可無影響理解的部分,只留下了最精華的部分,我不知道如果我一開始接觸的是這樣的程式碼是否能夠理解到位。

鳥瞰整段程式碼,大概分為三塊。

  • 第一塊用二進位制方式網記憶體中寫了資料(四個段描述符),並定義了三個常量
  • 第二塊其實仔細觀察會發現就是進入保護模式的步驟(開啟A20、載入gdt、將cr0的pe位置1)
  • 第三塊還是一個在螢幕上輸出“32mod”字串,與之前不同的是這是在保護模式下的輸出

三、程式碼第一塊解讀:全域性段描述符表(GDT)

cpu 與作業系統打配合的方式

有件事現在說可能體會不大,寫到後面好多地方你會發現,像載入 gdt 這種操作模式好多地方都是通用的,咱先不用管 gdt 是什麼,總之 cpu 會有很多與作業系統相互打配合的地方,這個就是其中之一。配合怎麼打呢,那就是 cpu 定義好一個數據結構,再給你一個暫存器。作業系統一般負責做三件事情

  1. 負責在記憶體中某位置按照這個資料結構寫一堆資料(如本講的段描述符表gdt,以及之後要說的頁表)
  2. 然後再把你寫在記憶體的哪個位置這個資訊(起始地址),存在 cpu 給你預留的一個暫存器裡,這一般會有一條專門的指令,比如本講的 lgdt,不會說讓你用 mov 操作的
  3. 作業系統將 cpu 某暫存器中的某位置 1

然後就開啟了這個功能,段描述符表如此,頁表如此,TSS亦是如此,這個之後講到會深有體會。我現在已經有所體會了,但還沒整理出全部的這種打配合的地方,等我再深入些再給大家整理一份。

先說說什麼是段描述符

直接上乾貨,還記不記得第一節課說的內容

在你開機的一瞬間,CPU 的 PC 暫存器被強制初始化為 0xFFFF0。如果再說具體些,CPU 將段基址暫存器 cs 初始化為 0xF000,將偏移地址暫存器 IP 初始化為 0xFFF0,根據真實模式下的最終地址計算規則,將段基址左移 4 位,加上偏移地址,得到最終的實體地址也就是抽象出來的 PC 暫存器地址為 0xFFFF0。

這種段基址左移 4 位,加上偏移地址,得到實體地址的方式,就是真實模式下的地址轉換方式。

然而保護模式下不一樣了

在保護模式下,段基址暫存器中存的資料,被理解為段選擇子,根據這個值去我們自己在記憶體中寫好的段描述符表中找,找到對應的段描述符,從中取出段基址。用這個段基址加上偏移地址,最終得到實體地址(邏輯地址和頁表的事以後再說,不衝突)。

就這麼點區別

那自然就有兩個問題,一個是段描述符表長什麼樣子呀?決定了我們往記憶體中寫的資料結構是什麼。另一個就是去哪找段描述符表壓,這個就需要告訴 cpu 為我們提前預留好的暫存器,也就是 lgdt 指令。下面我們就分別看著兩個問題

段描述符表長什麼樣子

首先段描述符表是一張表,在記憶體中也就是個陣列,是一個個的段描述符一個個緊挨著的結果。所以我們要了解段描述符長什麼樣就好了

這裡我順便把選擇子和 GDTR 暫存器的結構也列出來了,這些就是全部的需要我們自己寫資料的地方了,也是 cpu 和作業系統配合中需要約定的全部事情

;0描述符
    dd  0x00000000
    dd  0x00000000
;1描述符(4GB程式碼段描述符)
    dd  0x0000ffff
    dd  0x00cf9800
;2描述符(4GB資料段描述符)
    dd  0x0000ffff
    dd  0x00cf9200
;3描述符(28Kb的視訊段描述符)
    dd  0x80000007
    dd  0x00c0900b

我們看看這些直接在記憶體中寫死的常量,就是按照段描述符的資料結構寫的

程式碼段描述符轉化為二進位制是 00000000_00000000_11111111_11111111_00000000_11001111_10011000_00000000
資料段描述符轉為為二進位制是 00000000_00000000_11111111_11111111_00000000_11001111_10010010_00000000
視訊段描述符轉化為二進位制是 10000000_00000000_00000000_00000111_00000000_11000000_10010000_00000000

這裡我們拿視訊段描述符來分析,提取(拼湊)出段基址的資料,00000000_00000000_10000000_00000000,轉換為十六進位制是 0x80000。怎麼樣熟不熟悉,這恰好是顯示卡黑白模式在記憶體中的對映的起始地址。可以看下第一章的內容,不過我這裡還是把圖貼出來。

接下來的幾個常量定義,很容易明白它們的意思

lgdt_value:
    dw $-gdt-1  ;高16位表示表的最後一個位元組的偏移(表的大小-1) 
    dd gdt      ;低32位表示起始位置(GDT的實體地址)

SELECTOR_CODE   equ 0x0001<<3
SELECTOR_DATA   equ 0x0002<<3
SELECTOR_VIDEO  equ 0x0003<<3

lgdt_value 就是按照 lgdt 暫存器規定的資料結構拼湊出來的,下面的三個常量其實就是對應上面定義的三個段描述符的偏移量,由於每個描述符佔 64 位,也就是佔 8 個地址單元,所以索引下標的計算就是第幾個描述符 * 8就好了,相信這個不難理解。

四、程式碼第二塊解讀:進入保護模式三步走

程式碼直接對應上面的三步

載入 gdt

lgdt [lgdt_value]

開啟 A20

in al,0x92
or al,0000_0010b
out 0x92,al
cli    ;禁止中斷,先不用管

將 cr0 的 pe 位置 1

mov eax,cr0
or eax,1
mov cr0,eax

此時已經進入保護模式了,段基址暫存器的意義已經變了,所以跳轉指令變成了

jmp dword SELECTOR_CODE:main

五、程式碼第三塊解讀:保護模式下的簡單程式碼

前面就是將資料段暫存器賦值給一些段基址暫存器用於訪問資料段,然後將棧基址賦值位本次載入到的記憶體位置,重點是下面幾句

mov ax,SELECTOR_VIDEO
mov gs,ax
mov byte [gs:0xa0],'3'
...

這段將我們剛剛寫好的常量 SELECTOR_VIDEO 寫入了段基址暫存器 gs,並在其後用了這個基址暫存器去進行 mov 操作。通過這個段選擇子,在段描述符表裡尋找出來的段基址是我們寫好的顯示卡的記憶體對映的起始地址,所以同前幾章在真實模式下的輸出就一樣了。

六、執行程式碼

我們並沒有增加新檔案,所以Makefile和上一篇一樣,不用變,直接執行看效果,make brun

可以看到,我們的段基址暫存器沒有直接寫顯示卡的起始地址,而是通過段選擇子索引的,但依然正常輸出了 "32mod" 字串,說明成功了

寫在最後:開源專案和課程規劃

如果你對自制一個作業系統感興趣,不妨跟隨這個系列課程看下去,甚至加入我們,一起來開發。

參考書籍

《作業系統真相還原》這本書真的贊!強烈推薦

專案開源

專案開源地址:https://gitee.com/sunym1993/flashos

當你看到該文章時,程式碼可能已經比文章中的又多寫了一些部分了。你可以通過提交記錄歷史來檢視歷史的程式碼,我會慢慢梳理提交歷史以及專案說明文件,爭取給每一課都準備一個可執行的程式碼。當然文章中的程式碼也是全的,採用複製貼上的方式也是完全可以的。

如果你有興趣加入這個自制作業系統的大軍,也可以在留言區留下您的聯絡方式,或者在 gitee 私信我您的聯絡方式。

課程規劃

本課程打算出系列課程,我寫到哪覺得可以寫成一篇文章了就寫出來分享給大家,最終會完成一個功能全面的作業系統,我覺得這是最好的學習作業系統的方式了。所以中間遇到的各種坎也會寫進去,如果你能持續跟進,跟著我一塊寫,必然會有很好的收貨。即使沒有,交個朋友也是好的哈哈。

目前的系列包括

  • 【自制作業系統01】硬核講解計算機的啟動過程
  • 【自制作業系統02】環境準備與啟動區實現
  • 【自制作業系統03】讀取硬碟中的資料
  • 【自制作業系統04】從真實模式到保護模式