1. 程式人生 > 實用技巧 >自制作業系統(4)進入保護模式

自制作業系統(4)進入保護模式

進入保護模式

進入保護模式後才能進行32位定址,進而才有可能寫出GUI介面

整個保護模式非常複雜,這裡一步一步來慢慢說

首先來看一下全域性描述表的一個數據結構:

GDT Description

[BYTE7] [BYTE6(1)] [BYTE6(2)] [BYTE5] [BYTE4 BYTE3 BYTE2] [BYTE1 BYTE0]

這個資料結構用匯編表示如下:

pm.inc

%macro Descriptor 3
    dw    %2  &  0FFFFh ;段界限1
    dw    %1  &  0FFFFh ;段基址1
    db   (%1>>16) & 0FFh ;段基址2
    dw   ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ;屬性1+段界限2+屬性2
    db   (%1 >> 24) & 0FFh ;段基址3
%endmacro

DA_32		EQU	4000h ;32位段
DA_C		EQU	98h ;存在的只執行程式碼段屬性值
DA_DRW		EQU	92h	;存在的可讀寫資料段屬性值

段界限1如果是0&FFFF=0,如果是ABCD,那麼ABCD&FFFF=ABCD

所以說這裡的&的作用其實並沒有讓這個值發生變化

段基址也是這樣,段基址1沒問題

段基址2這裡00 0B 80 00 00>>16=00 00 00 0B 80

再&FF等於80,正確拿到了段基址2

段界限總共是20位,段界限1是16位,所以段界限2這裡是4位

假如段界限是12345,12345>>8=00123,00123&0F00=00100,成功取到了最高位

屬性這裡也就好理解了,&F0FF說明0這裡做一個清零的操作,因為這裡是段界限2

核心程式碼如下:

kernel.asm

%include "pm.inc" ;引入一個數據結構

org   0x9000 ;從0x9000往後載入

jmp   LABEL_BEGIN

 ;Descriptor是GDT的資料結構
 ;CODE32中段界限是計算出的段長
 ;VIDEO中顯示卡在記憶體中的對映是0B8000H,段界限是0ffff,屬性表示可讀可寫
[SECTION .gdt]
 ;                                  段基址          段界限                屬性
LABEL_GDT:          Descriptor        0,            0,                   0
LABEL_DESC_CODE32:  Descriptor        0,      SegCode32Len - 1,       DA_C + DA_32 
LABEL_DESC_VIDEO:   Descriptor     0B8000h,         0ffffh,            DA_DRW

GdtLen     equ    $ - LABEL_GDT ;當前的地址減去LABEL_GDT,所以GdtLen就是24(3個8位)
GdtPtr     dw     GdtLen - 1 ;表示三個描述符的總長度
           dd     0 ;dd是32位4位元組,表示描述符表的起始地址(具體如何計算出在下面的彙編程式碼)

SelectorCode32    equ   LABEL_DESC_CODE32 -  LABEL_GDT ;CODE32相對於GDT的地址偏移,也就是8位元組
SelectorVideo     equ   LABEL_DESC_VIDEO  -  LABEL_GDT ;VIDEO相對於GDT的地址偏移,也就是16位元組

[SECTION  .s16]
[BITS  16]

LABEL_BEGIN:
     ;初始化一堆暫存器
     mov   ax, cs
     mov   ds, ax
     mov   es, ax
     mov   ss, ax
     mov   sp, 0100h

     ;這一段完成了GDT中關於基址地址的初始化
     ;真實模式下的定址方式:段暫存器*16+偏移(16位)
     ;EAX [12 34 AB CD]
     ;EX [AB CD]
     ;AL [CD]
     ;AH [AB]
     xor   eax, eax ;把eax暫存器清零
     mov   ax,  cs ;cs暫存器中儲存的是程式碼段的段基址(ax是eax的低16位)
     shl   eax, 4 ;向左移4位,實現段暫存器*16,也就是ABCD->ABCD0
     add   eax, LABEL_SEG_CODE32 ;段暫存器*16+偏移得到地址
     mov   word [LABEL_DESC_CODE32 + 2], ax ;eax的低16位存入LABEL_DESC_CODE32偏移兩位元組
                                            ;也就是寫入了LABEL_DESC_CODE32中的段基址1
     shr   eax, 16 ;這時候eax的高16位轉移到ax,也就是1234ABCD->00001234
     mov   byte [LABEL_DESC_CODE32 + 4], al ;把ax的低8位放入LABEL_DESC_CODE32中的段基址2
     mov   byte [LABEL_DESC_CODE32 + 7], ah ;把ax的高8位放入LABEL_DESC_CODE32中的段基址3
                                            ;到這裡就完整的初始化了一個Descriptor資料結構

     xor   eax, eax
     mov   ax, ds 
     shl   eax, 4
     add   eax,  LABEL_GDT
     mov   dword  [GdtPtr + 2], eax ;GdtPtr+2就是上面的dd 0
                                    ;eax就是LABEL_GDT的地址
                                    ;三個資料結構起始的地址放入了dd中
                                    ;由這三個資料結構組成的列表就是全域性描述符表,也就是GDT

     lgdt  [GdtPtr] ;BIOS中斷呼叫,將全域性描述符表載入到CPU中

     cli   ;關中斷(下面的這些程式碼不能被滑鼠和鍵盤的操作影響)

     in    al,  92h ;得到92h埠的資料
     or    al,  00000010b ;把第1位置為1
     out   92h, al ;然後把資料寫回92h埠

     mov   eax, cr0 ;從特殊暫存器中讀取值
     or    eax , 1 ;把最低位的值置1
     mov   cr0, eax ;然後把資料寫回cr0暫存器

     jmp   dword  SelectorCode32: 0 ;得到Descriptor資料結構並取2347位元組組成完整的地址並JMP
                                    ;這裡也是一個偏移定址,dword是4位元組32位

[SECTION .s32]
[BITS  32]

LABEL_SEG_CODE32: ;上面的JMP跳到了這裡,從這裡開始就是所謂的保護模式
    mov   ax, SelectorVideo ;把Descriptor資料結構的地址放入ax
    mov   gs, ax ;真實模式的定址方式發生了變化,這裡是拿到Video的Descriptor資料結構地址
                 ;然後找到該資料結構中由2347位元組構成的基地址(這裡代表視訊記憶體基地址)
                 ;從該地址開始,每2位元組用來顯示一個字元,第1個位元組表示顏色,第2個位元組表示ASCII值
    mov   si, msg
    mov   ebx, 10 ;從第10列開始顯示字元
    mov   ecx, 2 ;顯示一個字元需要兩個位元組

showChar:
    mov   edi, (80*11) ;從第11行開始顯示字元
                       ;1行有80個字元
    add   edi, ebx     ;80*11+10
    mov   eax, edi
    mul   ecx ;每個字元需要2個位元組,所以最終eax結果是(80*11+10)*2
    mov   edi, eax ;最終計算得出第11行10列所在的視訊記憶體位置
    mov   ah, 0ch ;第1個位元組設定顏色
    mov   al, [si] ;取si暫存器地址指向的值,也就是輸出字串
    cmp   al, 0 ;如果al是0,那麼進入死迴圈
    je    end
    add   ebx,1 ;列標號+1
    add   si, 1 ;指向字元的指標+1(也就是讀取字串的下一個字元)
    mov   [gs:edi], ax ;al和ah一共2位元組,正好放入視訊記憶體地址指向的值
    jmp    showChar ;迴圈

end: 
    jmp   $  

msg:
    DB     "Protect Mode", 0

SegCode32Len   equ  $ - LABEL_SEG_CODE32 ;當前的地址-LABEL_SEG_CODE32程式碼段地址


編譯二進位制檔案再生成system.img成功執行

下面做一個小改變,嘗試申請5M的記憶體,然後在記憶體裡面寫入msg並讀出:

boot_read5M.asm

%include "pm.inc"

org   0x7c00

jmp   LABEL_BEGIN

[SECTION .gdt]
 ;                                  段基址          段界限                屬性
LABEL_GDT:          Descriptor        0,            0,                   0  
LABEL_DESC_CODE32:  Descriptor        0,      SegCode32Len - 1,       DA_C + DA_32
LABEL_DESC_VIDEO:   Descriptor     0B8000h,         0ffffh,           DA_DRW
LABEL_DESC_5M:      Descriptor     0500000h,        0ffffh,           DA_DRW ;加入這一行

GdtLen     equ    $ - LABEL_GDT
GdtPtr     dw     GdtLen - 1
           dd     0

SelectorCode32    equ   LABEL_DESC_CODE32 -  LABEL_GDT
SelectorVideo     equ   LABEL_DESC_VIDEO  -  LABEL_GDT
Selector5M        equ   LABEL_DESC_5M - LABEL_GDT ;加入這一行,得到距離GDT的偏移,也就是LABEL_DESC_5M的起始地址

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

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

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

     lgdt  [GdtPtr]

     cli

     in    al,  92h
     or    al,  00000010b
     out   92h, al

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

     jmp   dword  SelectorCode32: 0

[SECTION .s32]
[BITS  32]

LABEL_SEG_CODE32:
    mov   ax, SelectorVideo
    mov   gs, ax

    mov   si, msg
    mov   ax, Selector5M ;用es指向5M記憶體描述符
    mov   es, ax
    mov   edi, 0

write_msg_to_5M: ;將si指向的字元寫到5M記憶體處
    cmp   byte [si], 0 ;是否讀完
    je    prepare_to_show_char ;if(si[i]==0){prepare_to_show_char();}
    mov   al, [si]
    mov   [es:edi], al ;es暫存器是Selector5M的偏移值,進而得到LABEL_DESC_5M的基地址
                       ;edi=0,所以意思是把al暫存器的值寫入LABEL_DESC_5M的基地址偏移0的地址記憶體中
    add   edi, 1 ;偏移+1
    add   si, 1 ;相當於for迴圈的i++
    jmp   write_msg_to_5M ;迴圈方式


prepare_to_show_char:
    mov   ebx, 10
    mov   ecx, 2
    mov   si, 0

showChar:
    mov   edi, (80*11)
    add   edi, ebx
    mov   eax, edi
    mul   ecx
    mov   edi, eax
    mov   ah, 0ch
    mov   al, [es:si] ;由於es指向描述符LABEL_DESC_5M,所以es:si表示的地址是從5M開始的記憶體,si表示從5M開始後的偏移
                      ;所以這句話可以理解為字串從第一個開始遍歷放入al
    cmp   al, 0
    je    end ;if(al==0){end();}
    add   ebx,1
    add   si, 1
    mov   [gs:edi], ax ;組合ah和al然後放入視訊記憶體中
    jmp   showChar
end: 
    jmp   $
msg:
    DB     "This string is writeen to 5M memory", 0

SegCode32Len   equ  $ - LABEL_SEG_CODE32

nasm kernel.asm -o kernel.bat

nasm boot_read5M.asm -o boot.bat

編譯二進位制檔案再生成system.img成功執行