天天看點

java開發作業系統核心:由實模式進入保護模式之32位尋址

從時模式到保護模式,是計算法技術跨時代的發展。大家想想笨拙的Dos界面,黑底白字的那種冷漠界面到win95各種色彩斑斓的視窗,兩者之間的差別其實就是實模式和保護模式的天壤之别。

保護模式中,最重要的一個概念莫過于”保護”二字,有了“保護”功能後,CPU為軟體提供了很多的功能,當然也有了更多的限制。要詳細解析保護機制,沒有幾千字的“鴻篇巨制”是不可能的,但我想你和我一樣不會有那樣的耐心,由此話不多說,我們先将代碼跑起來再說:

%include "pm.inc"

org   

jmp   LABEL_BEGIN

[SECTION .gdt]
 ;                                  段基址          段界限                屬性
LABEL_GDT:          Descriptor        ,            ,                     
LABEL_DESC_CODE32:  Descriptor        ,      SegCode32Len - ,       DA_C + DA_32
LABEL_DESC_VIDEO:   Descriptor     B8000h,         ffffh,            DA_DRW

GdtLen     equ    $ - LABEL_GDT
GdtPtr     dw     GdtLen - 
           dd     

SelectorCode32    equ   LABEL_DESC_CODE32 -  LABEL_GDT
SelectorVideo     equ   LABEL_DESC_VIDEO  -  LABEL_GDT

[SECTION  .s16]
[BITS  ]
LABEL_BEGIN:
     mov   ax, cs
     mov   ds, ax
     mov   es, ax
     mov   ss, ax
     mov   sp, h

     xor   eax, eax
     mov   ax,  cs
     shl   eax, 
     add   eax, LABEL_SEG_CODE32
     mov   word [LABEL_DESC_CODE32 + ], ax
     shr   eax, 
     mov   byte [LABEL_DESC_CODE32 + ], al
     mov   byte [LABEL_DESC_CODE32 + ], ah

     xor   eax, eax
     mov   ax, ds
     shl   eax, 
     add   eax,  LABEL_GDT
     mov   dword  [GdtPtr + ], eax

     lgdt  [GdtPtr]

     cli   ;關中斷

     in    al,  h
     or    al,  b
     out   h, al

     mov   eax, cr0
     or    eax , 
     mov   cr0, eax

     jmp   dword  SelectorCode32: 

     [SECTION .s32]
     [BITS  ]
LABEL_SEG_CODE32:
    mov   ax, SelectorVideo
    mov   gs, ax
    mov   si, msg
    mov   ebx, 
    mov   ecx, 
showChar:
    mov   edi, (*)
    add   edi, ebx
    mov   eax, edi
    mul   ecx
    mov   edi, eax
    mov   ah, ch
    mov   al, [si]
    cmp   al, 
    je    end
    add   ebx,
    add   si, 
    mov   [gs:edi], ax
    jmp    showChar
end: 
    jmp   $
    msg:
    DB     "Protect Mode", 

SegCode32Len   equ  $ - LABEL_SEG_CODE32

           

我們再看看pm.inc的内容:

%macro Descriptor 
    dw    %2  &  0FFFFh
    dw    %1  &  0FFFFh
    db   (%1>>) & 0FFh
    dw   ((%2 >> ) & 0F00h) | (%3 & 0F0FFh)
    db   (%1 >> ) & 0FFh
%endmacro


DA_32       EQU h   ;  位段
DA_C        EQU h ; 存在的隻執行代碼段屬性值
DA_DRW      EQU h ; 存在的可讀寫資料段屬性值
           

把上面的代碼通過以下指令編譯:

nasm -o kernel.bat kernel.asm

在目錄下會生成一個核心檔案,kernel.bat,運作java代碼,程式會将生成的核心寫入到虛拟軟碟中,将虛拟軟碟加載到虛拟機中,運作後會得到如下結果:

我們看看代碼,從LABEL_SEG_CODE32:這一部分開始,代碼就執行在保護模式下,這段代碼的作用就是顯示一串字元,gs是計算機的一個寄存器,它跟eax,ebx這些寄存器差不多,但作用更為單一,主要用來指向顯存,當我們将資訊寫入gs指向的記憶體後,資訊會顯示到螢幕上。用于顯示字元的顯存,記憶體位址從0XB800h開始,從該位址開始,每兩個位元組用來在螢幕上顯示一個字元,這兩個位元組中,第一個位元組的資訊用來表示字元的顔色,第二個位元組用來存儲要顯示的字元的ASCII值,螢幕一行能顯示80個字元,大家看到代碼中有語句:

mov edi, (80*11)

這表明我們要從第11行開始顯示字元,接下來又有語句:

add edi, ebx

其中,ebx的值是11,這表明我們要從第11行的第10列開始顯示字元串,接下來的語句是:

mov eax, edi

mul ecx

ecx的值是2,這個2就是我們前面說過的顯示一個字元需要兩個位元組,上面幾句彙編語句的作用是:

eax = ((80*11) + 10) * 2

這樣eax就指向了第11行第11列所在的顯存位置,接下來語句:

mov ah, 0ch

它的作用是在用來顯示字元的兩位元組中,對第一個位元組放入數值0ch,也就是設定字元的顔色,接下來的語句:

mov al, [si]

将寄存器si指向的字元的ascii值寫入到第二個位元組,這樣,字元就顯示到螢幕上了。大家注意寄存器si的用法:[si]. si相當于C語言中的一個指針,指向記憶體某個位址,[si]就是讀取si指向的記憶體位址的資訊,等同于 c語言中的*(si)

以上都是小細節,真正的要點是,我們要了解什麼叫保護模式。我們先看保護模式的兩個顯著特點:

1.尋址空間從時模式的1M增強到4G

2.不同的代碼擁有不同的優先級,優先級高的能夠執行特殊指令,優先級低的,某些重要指令就無法執行。

于是進入保護模式,我們需要解決兩個問題,一是如何擷取超過1M以上的記憶體位址,第二是如何設定不同代碼所具有的優先級。我們先看看尋找能力的變化,在實模式下,cpu是16位的,寄存器16位,資料總線16位,位址總線20位,于是尋找的範圍必然受限于20位的位址總線,是以尋找範圍無法超過1M(2^20).要想實作4GB的尋址,我們必須使用32位來表示位址,intel是這麼解決這個問題的,他們用連續的8個位元組組成的結構體來解決一系列問題:

byte0

byte1

…..

byte7

其中,位元組2,3,4以及位元組7,這四個位元組合在一起總共有32位,這就形成了一個32位的位址。同時把位元組0,位元組1,以及将位元組6的拆成兩部分,各4個bits,前4個bits跟位元組0,位元組1合在一起,形成一個20個bit的資料,用來表示要通路的記憶體長度。這樣,我們就解決了記憶體尋址的問題。

大家或許猜到,pm.inc裡面的宏定義就是我們說的7位元組資料結構,

%macro Descriptor 3

表示要初始化該資料結構,需要傳入3個參數,%1表示引用第一個參數,%2表示引用第二個參數。初始化該結構時,輸入的一個參數是記憶體的位址,大家看語句:

dw %1 & 0FFFFh

db (%1>>16) & 0FFh

這兩句就是把記憶體位址的頭三個位元組放入到byte2,byte3,byte4,最後一句:

db (%1 >> 24) & 0FFh

就是講位址的第4個位元組放入到byte7. 初始化資料結構的第二個參數表示的是要通路的記憶體的長度,大家看語句:

dw %2 & 0FFFFh

就是把記憶體長度的頭兩個位元組寫入byte0,byte1,語句:

dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)

中的((%2 >> 8) & 0F00h)就是把記憶體長度的第16-19bit寫入到byte6的前4個bit.由此要通路的記憶體和記憶體的長度就都設定好了,由于保護模式是一個非常複雜的邏輯,為了掌握它,我們一次隻吃透一點,這樣才好掌握,本章,我們隻要了解如何進行32位的尋找就足夠了,其他的知識我們後面會一點一滴的分析。

大家看開頭的幾條語句:

xor   eax, eax
     mov   ax,  cs
     shl   eax, 
     add   eax, LABEL_SEG_CODE32
     mov   word [LABEL_DESC_CODE32 + ], ax
     shr   eax, 
     mov   byte [LABEL_DESC_CODE32 + ], al
     mov   byte [LABEL_DESC_CODE32 + ], ah
           

LABEL_SEG_CODE32是一段代碼的起始位址,上面的語句就是将這個起始位址寫入到byte2,byte3,byt4,和byte7.大家是否會疑惑,為什麼不在初始化時将這個位址直接傳進去呢,例如:

LABEL_DESC_CODE32: Descriptor LABEL_SEG_CODE32, SegCode32Len - 1, DA_C + DA_32

這是因為,結構體初始化時隻能傳入常量,LABEL_SEG_CODE32所代表的數值需要編譯器在将代碼編譯完後才能計算出來,是以LABEL_SEG_CODE32一開始的值還不能确定,是以不能直接用于初始化結構體。

以上的代碼大家可以通過指令擷取:

git clone https://github.com/wycl16514/OS-Kernel-from-real-to-protected-mode.git

大家看完這一章後,可能有一點感性認識,但更有可能是落入到一種雲裡霧裡,不知所措的狀态,這不要緊,在後面,我還會有很多篇幅來解釋保護模式,大家的迷惑會在後面的閱讀中一點一點的給驅散掉,面對一個複雜棘手的問題,将其分解,各個擊破是最好的政策,隻要我們有耐心,一步一個腳印,我們就會經曆一個山窮水盡疑無路,柳暗花明又一村的體驗。