本次實踐的目的:打破開機引導程式512位元組的限制,并從實模式切換到保護模式。
我們知道,bios開機自檢、找到啟動裝置後,把啟動裝置的第一個扇區加載記憶體0x7c00位置開始執行。前兩次實踐中,我們的引導程式小于512位元組,這沒造成什麼問題。如果我們的引導程式超過512位元組怎麼辦呢?我的第一個想法就是,利用加載到記憶體的這512位元組,寫個程式,把啟動盤中真正的引導程式繼續加載到記憶體中。看到《Orange’s 一個作業系統的實作》的第三章的時候,裡面并沒有采用這種做法,而是轉而去用個DOS加載大于512位元組的引導程式,那就自己動手寫一個吧。
程式思路
- 寫個少于512位元組的引導程式,用于啟動引導後,把軟碟第2扇區(如果你寫的代碼多與512位元組,需要修改下面的引導,這裡簡單隻拷貝了1個扇區)的資料拷貝到 0x7e00 的位置(0x7e00 == 0x7c00 + 512)。具體實作是調用了bios的13h中斷。
- 保護模式的尋址方式與實模式的尋址方式不同。 雖然從實模式到保護模式隻需要設定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’):
問題
-
gdt、gdtr結構如何?
這個雖然在不同書籍都看過了,沒親自寫代碼,還是會忘掉。網上的版本請參考: 《GDT 與 LDT》。
-
gdtr limit字段如何設定?為什麼用gdt長度減一?
參考gdt的limit字段,其實不應該了解為長度,而是與offset類似的,從0開始,比如說計算gdtr最高的位址的時候,就可以用基址+limit計算出來。
-
打開a20線還有其它方法嗎?
這個問題還沒搞清楚,回頭更新這裡。
-
在16位模式下jmp dword SelectorCode32:0 其中的dword 是修飾哪個?linux核心中用db寫二進制是如何實作的?
按目前的了解,選擇子隻有13位有效,是以 SelectorCode32這個用16位足以,而後面的偏移量offset,則可以是32位的。這句代碼反彙編之後結果為:
00007e71: 66ea 0000 0000 0800 jmpf 0x0008:0000 0000
至于linux核心如何實作,後續跟進。我猜想就是直接用類似上面的二進制代碼方式實作。