天天看點

作業系統實踐(3)——火箭助推器

本次實踐的目的:打破開機引導程式512位元組的限制,并從實模式切換到保護模式。

我們知道,bios開機自檢、找到啟動裝置後,把啟動裝置的第一個扇區加載記憶體0x7c00位置開始執行。前兩次實踐中,我們的引導程式小于512位元組,這沒造成什麼問題。如果我們的引導程式超過512位元組怎麼辦呢?我的第一個想法就是,利用加載到記憶體的這512位元組,寫個程式,把啟動盤中真正的引導程式繼續加載到記憶體中。看到《Orange’s 一個作業系統的實作》的第三章的時候,裡面并沒有采用這種做法,而是轉而去用個DOS加載大于512位元組的引導程式,那就自己動手寫一個吧。

程式思路

  1. 寫個少于512位元組的引導程式,用于啟動引導後,把軟碟第2扇區(如果你寫的代碼多與512位元組,需要修改下面的引導,這裡簡單隻拷貝了1個扇區)的資料拷貝到 0x7e00 的位置(0x7e00 == 0x7c00 + 512)。具體實作是調用了bios的13h中斷。
  2. 保護模式的尋址方式與實模式的尋址方式不同。 雖然從實模式到保護模式隻需要設定cr0寄存器即可,但是切換過去後,其尋址依賴于GDT的實作,是以需切換前先設定好GDT。

bf.asm

org 07c00h
    [BITS 16]

START:
    mov ax,cs
    mov ds,ax
    mov es,ax

    ;拷貝軟碟中的代碼到記憶體區
COPY:
    mov bx, COPY_CODE_START ;07c00h + 512(0100h) == 07e00h
    mov dl,0        ;驅動器号,軟驅從0開始:0:軟驅A,1:軟驅B
                    ;磁盤從80h開始,80h:C槽,81h:D盤
    mov dh,0        ;磁頭号,對于軟碟即面号,一個面用一個磁頭來讀寫
    mov ch,0        ;磁道号
    mov cl,2        ;扇區号
    mov al,2        ;讀取的扇區數
    mov ah,2        ;13h的功能号(2表示讀扇區),es:bx指向
                    ;接收從扇區讀入資料的記憶體區
    int 13h
    jc COPY         ;讀取失敗,CF表示為1,重試讀取   

    jmp LABEL_BEGIN ;把程式讀到記憶體區後,跳轉到新的執行點

    ;補全512位元組
    times 510-($-$$) db 0
    dw 0xaa55


;這個宏用來填充gdt描述符的,每個描述符8個位元組,64位。
;參數1:段基址,32位
;參數2:段大小limit,傳32位,隻用其低20位。
;參數3:段屬性,16位,隻用高4位與低8位,中間4位為0。
%macro Descriptor 3
        dw %2 & 0FFFFh
        dw %1 & 0FFFFh
        db (%1 >> 16) & 0FFh
        dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)
        db (%1 >> 24) & 0FFh
%endmacro

;段屬性的常量,具體參考《Orange's 一個作業系統的實作》
DA_32   equ     4000h

DA_DPL0 equ     00h
DA_DPL1 equ     20h
DA_DPL2 equ     40h
DA_DPL3 equ     60h

DA_DR   equ     90h
DA_DRW  equ     92h
DA_DRWA equ     93h
DA_C    equ     98h
DA_CR   equ     9ah
DA_CCO  equ     9ch
DA_CCOR equ     9eh

DA_LDT  equ     82h
DA_TaskGate     equ     85h
DA_386TSS       equ     89h
DA_386CGate     equ     8ch
DA_386IGate     equ     8eh
DA_386TGate     equ     8fh

COPY_CODE_START:

;全局描述符GDT,在切換到保護模式前,需先設定好相應的描述符。
[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
GdtPtr  dw GdtLen - 1
        dd 0

SelectorCode32  equ     LABEL_DESC_CODE32 - LABEL_GDT
SelectorVideo   equ     LABEL_DESC_VIDEO - LABEL_GDT

;這段16位的代碼段,目的是實作從實模式到保護模式的切換。
[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

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

        ;加載gdt
        lgdt [GdtPtr]

        cli

        ;打開A20位址線,擴大尋址空間
        in al,92h
        or al,00000010b
        out 92h,al

        ;從實模式切換到保護模式
        mov eax,cr0
        or eax,1
        mov cr0,eax

        ;跳轉到32位的保護模式的代碼
        jmp dword SelectorCode32:0

;這段代碼的功能,隻是在螢幕右邊的中間位置顯示一個黑底紅色的字母'P'
[SECTION .s32]
[BITS 32]
LABEL_SEG_CODE32:
        mov ax,SelectorVideo
        mov gs,ax

        mov edi,(80 * 11 + 79) *2
        mov ah,0ch
        mov al,'P'
    mov [gs:edi],ax

        jmp $

SegCode32Len equ $ - LABEL_SEG_CODE32
           

這裡為了省去每次都敲一堆指令的麻煩,寫了個簡單的腳本

bf.sh

#!/bin/bash
/usr/bin/nasm bf.asm -o bf.bin
dd if=bf.bin of=bf.img bs=512 count=2 conv=notrunc
bochs -f bf.bochs #這裡的bf.bochs配置檔案,請參考前一節的配置           

執行

./bf.sh

結果下圖(螢幕右邊中間位置有個紅色的’P’):

作業系統實踐(3)——火箭助推器

問題

  1. gdt、gdtr結構如何?

    這個雖然在不同書籍都看過了,沒親自寫代碼,還是會忘掉。網上的版本請參考: 《GDT 與 LDT》。

  2. gdtr limit字段如何設定?為什麼用gdt長度減一?

    參考gdt的limit字段,其實不應該了解為長度,而是與offset類似的,從0開始,比如說計算gdtr最高的位址的時候,就可以用基址+limit計算出來。

  3. 打開a20線還有其它方法嗎?

    這個問題還沒搞清楚,回頭更新這裡。

  4. 在16位模式下jmp dword SelectorCode32:0 其中的dword 是修飾哪個?linux核心中用db寫二進制是如何實作的?

    按目前的了解,選擇子隻有13位有效,是以 SelectorCode32這個用16位足以,而後面的偏移量offset,則可以是32位的。這句代碼反彙編之後結果為:

    00007e71: 66ea 0000 0000 0800 jmpf 0x0008:0000 0000

    至于linux核心如何實作,後續跟進。我猜想就是直接用類似上面的二進制代碼方式實作。

繼續閱讀