
通過前三章的努力,我們成功将控制權轉交給了 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 也有實模式)
實模式與保護模式的差別是什麼
- 實模式 16 位,保護模式 32 位
- 實模式下的位址是段寄存器位址偏移4位+偏移位址得到實體位址。保護模式下段寄存器存入了段選擇子,在段描述符表中尋找段基址,再加上偏移位址得到實體位址(開啟分頁下為邏輯位址)
- 這個我覺得是個 1 的推論,就是實模式尋址空間是 1M,保護模式是 4G
- 這個我覺得是 2 的推論,就是段描述符表記錄了段的權限,改變了實模式下可以随意通路所有記憶體的隐患(這也是保護這兩個字的展現)
怎麼進入保護模式
進入保護模式有三步:
- 打開 A20
- 加載 gdt
- 将 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 0x00c0920b
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 定義好一個資料結構,再給你一個寄存器。作業系統一般負責做三件事情
- 負責在記憶體中某位置按照這個資料結構寫一堆資料(如本講的段描述符表gdt,以及之後要說的頁表)
- 然後再把你寫在記憶體的哪個位置這個資訊(起始位址),存在 cpu 給你預留的一個寄存器裡,這一般會有一條專門的指令,比如本講的 lgdt,不會說讓你用 mov 操作的
- 作業系統将 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 0x00c0920b
我們看看這些直接在記憶體中寫死的常量,就是按照段描述符的資料結構寫的
代碼段描述符轉化為二進制是 00000000_00000000_11111111_11111111_00000000_11001111_10011000_00000000
資料段描述符轉為為二進制是 00000000_00000000_11111111_11111111_00000000_11001111_10010010_00000000
視訊段描述符轉化為二進制是 00000000_11000000_10010010_00001011_10000000_00000000_00000000_00000111
這裡我們拿視訊段描述符來分析,提取(拼湊)出段基址的資料,00000000_00001011_10000000_00000000,轉換為十六進制是 0xb8000。怎麼樣熟不熟悉,這恰好是顯示卡黑白模式在記憶體中的映射的起始位址。可以看下第一章的内容,不過我這裡還是把圖貼出來。
接下來的幾個常量定義,很容易明白它們的意思
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/fla shos
當你看到該文章時,代碼可能已經比文章中的又多寫了一些部分了。你可以通過送出記錄曆史來檢視曆史的代碼,我會慢慢梳理送出曆史以及項目說明文檔,争取給每一課都準備一個可執行的代碼。當然文章中的代碼也是全的,采用複制粘貼的方式也是完全可以的。
如果你有興趣加入這個自制作業系統的大軍,也可以在留言區留下您的聯系方式,或者在 gitee 私信我您的聯系方式。
課程規劃
本課程打算出系列課程,我寫到哪覺得可以寫成一篇文章了就寫出來分享給大家,最終會完成一個功能全面的作業系統,我覺得這是最好的學習作業系統的方式了。是以中間遇到的各種坎也會寫進去,如果你能持續跟進,跟着我一塊寫,必然會有很好的收貨。即使沒有,交個朋友也是好的哈哈。