預(yù)備知識:
- 熟悉 i386 CPU寄存器,了解實模式及保護模式模式;
- 了解A20門
- 文本模式下直接顯存操作
涉及工具:
NASM,一個文本編輯器(我用的是ConText + NASM語法高亮),QEMU/VMWARE虛擬機
前言:
近期確實很忙,論壇里有一位朋友寫的代碼進入不了保護模式;我最初也是對保護模式相當(dāng)敬畏,因為32位比16位要“復(fù)雜”的多;當(dāng)時一直不敢下手,偶爾的嘗試有如蜻蜓點水,但最終以失敗告終。在學(xué)校的圖書館里幾乎找不到386保護模式匯編的資料,更不用說CPU相關(guān)的書了;不知道看了多少可憐的教材后,終于湊出了一點起眼的代碼,不過還是失敗了。我最終是通過一個Bona Fide 的實例教程解決了問題:實例程序在我的開發(fā)過程中起到了很重要的作用。
由于時間原因,這篇文章將主要以代碼來說明,因為我的確沒太多的時間再去介紹“實模式”,“保護模式”,GDT,IDT,A20等等等等相關(guān)的名詞、概念及規(guī)范;這些東西在我的網(wǎng)站里已經(jīng)收羅了:http://www.xemean.net/resource/ ,其中有中文也有英文的,有的甚至是圖文并茂,網(wǎng)絡(luò)上也有不少的例子,但在這里強烈建議的一本電子教程是:《80X86保護模式教程》 ,這本書詳細(xì)地介紹了如何對 80X86 CPU進行編程,包括進入保護模式,保護模式的中斷,多任務(wù)等等。另外,值得一提的是:由楊季文等人編著的,清華大學(xué)出版社出版的《80x86匯編語言程序設(shè)計教程》也是一本不錯的書。
此文適合于有一定基礎(chǔ),但又不能實現(xiàn)保護模式切換的朋友。
本文將以我上次寫的“啟動你的計算機”的代碼為基礎(chǔ),演示進入保護模式,但程序還是在引導(dǎo)區(qū)內(nèi)工作。
姑且不理會保護模式下“復(fù)雜”的內(nèi)存管理,多任務(wù),中斷,實際上進入保護只要:
mov eax,cr0 ; 控制寄存器CR0 -> EAX
or eax,1 ; 最低位置1,即PE位
mov cr0,eax ; 寫回CR0
I386兼容CPU使用CR0這個寄存器來“控制”或者說決定CPU的工作狀態(tài),命名為“控制寄存器中”,其中CR0的最低位叫PE位,中文翻譯應(yīng)該是“保護模式允許”位,如果對CR0的PE位置1,則CPU就工作在保護模式下。不幸的是,我們并不能直接對CR0進行操作,但是卻可以通過通用寄存器對其修改,上面便是開啟保護模式大門的實例。當(dāng)然,只有上面的代碼你可能永遠(yuǎn)也進不了保護模式。
保護模式與實模式有一個區(qū)別在于,段寄存器不再保存實際的內(nèi)存地址,CPU已經(jīng)有32位尋址的能力,也就是能訪問4G的內(nèi)存,似乎用32位的EIP就可以訪問4G了,但Intel并沒有想得那么簡單,段寄存器在內(nèi)存管理方面還有很大的作用。另外,之所以叫保護模式,是因為CPU還能不同應(yīng)用層的代碼進行保護,這在16位實模式是做不到的。因此引入了GDT,及描述符的概念。(這就得請各位看官參看一些資料了)
CPU中有一個高速的寄存器用來保存GDT表在內(nèi)存中的位置以及GDT表的大小:GDT的大小用16位來表示,GDT的物理地址用32位來表示(以保證GDT能在4G內(nèi)存的任意位置),因此GDT高速寄存器(GDTR)占48位,已經(jīng)不能用一個32位的寄存器來表示了,因此要在內(nèi)存中表示出GDTR內(nèi)容,書上說這叫“偽描述符”,GDTR由下面的指令裝載:
lgdt [__GDTR]
其中__GDTR是GDT偽描述符的地址,一口氣,我們作如下數(shù)據(jù)定義:
ALIGN 4
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; GDT 偽描述符
; 參考保護模式相關(guān)文檔以獲得關(guān)于GDT偽描述
; 符更詳細(xì)的資料
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
__GDTR:
dw GDT_END - GDT-1 ; GDT表的長度,由編譯器計算
dd GDT ; GDT物理地址,由編譯器計算
;<- END OF __GDTR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; GDT entry
; 參考保護模式相關(guān)文檔以獲得關(guān)于GDT的更
; 詳細(xì)的資料
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ALIGN 8 ; 對齊,以保護CPU訪問GDT的速度
GDT:
; 第一個GDT作為保留項,以0填充
; reserved GDT
dd 0
dd 0
osCodeSel equ $-GDT ; 內(nèi)核用的代碼段選擇子
oscode:
dw 0xffff
dw 0
db 0
db 10011010b ; 0x9A ,可讀/可執(zhí)行 代碼段
db 11011111b ;
db 0
osDataSel equ $-GDT ; 內(nèi)核用的數(shù)據(jù)段選擇子
osdata:
dw 0xffff
dw 0
db 0
db 10010010b ; 0x92 ,可讀/寫 數(shù)據(jù)段
db 11011111b ;
db 0
GDT_END: ;<- END OF GDT
完成如上的定義之后,就可以著手進入保護模式了,過程大致為:
- 禁止所有中斷
- 打開A20門
- 加載GDTR
- 置PE位
- 初始化保護模式下的寄存器
- 一個遠(yuǎn)跳轉(zhuǎn)到32位代碼以清除當(dāng)前(實模式)的CS及EIP
如果跳轉(zhuǎn)成功,則CPU就可以工作在保護模式下。保護模式并不像實模式下有很多BIOS中斷可用,這就意味著我們必須自己寫鍵盤、顯卡等等驅(qū)動,計算機的幾乎所有資源都由內(nèi)核來管理,當(dāng)然,也由你來實現(xiàn)各種設(shè)備的驅(qū)動。
為了顯示我們的程序已經(jīng)成功地工作在保護模式下,我們必須在32位模式時在屏幕上寫點什么東西,直接寫顯存吧!演示程序?qū)︼@存進行操作,結(jié)果是屏幕的第三行第1列顯示了一個洋紅色的字母P。
源碼編譯:nasmw -f bin boot.asm -o boot.bin
用WinImage寫入軟盤鏡像,然后用Qemu或VMware啟動。注:不知道什么原因,這段代碼并不能在Bochs下工作。
拍照以示留念:
Image1.jpg
程序源碼如下:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; boot.asm; A demo to show how bootsect works; Last modified:2005-4-3 10:57:45 ; Copyright (c) 2005,E-mean X.;; This program is released under GPL,See document for details; You can use this code anywhere you want in condition keep autor's info; original;; Author: E-mean X.; Contact: xemean@sina.com; Website: http://www.xemean.net/; April,02,2005;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;**************************************************************************; 16位代碼bits 16
; 偽指令,告訴編譯器這是 16位代碼org 0x7C00
; 偽指令,告訴編譯器這段代碼由0x0:0x7C00開始;===========================================================================; 程序執(zhí)行的第一條指令必須是跳轉(zhuǎn)(如果你想使用FAT12這類文件系統(tǒng)的磁盤); 必須占用3字節(jié);===========================================================================jmp SHORT main
; 2 bits,跳轉(zhuǎn)到主程序執(zhí)行nop ; 1 bit;===========================================================================; FAT12 文件系統(tǒng)頭,從NYAOS 借過來的,可以參考相關(guān)的文檔以獲得更多細(xì)節(jié); 這個塊會讓 Winimage 認(rèn)出編譯后的二進制文件為有效的引導(dǎo)文件; 如果不使用這個塊,Winimage將不會將其作為引導(dǎo)程序處理; 但我們可以借助其它方法和工具處理,比如DEBUG;===========================================================================bsOEM
db "ExOS0.02" ; OEM String,任意你喜歡的8字節(jié)ASCII碼bsSectSize
dw 512
; Bytes per sectorbsClustSize
db 1
; Sectors per clusterbsRessect
dw 1
; # of reserved sectorsbsFatCnt
db 2
; # of fat copiesbsRootSize
dw 224
; size of root directorybsTotalSect
dw 2880
; total # of sectors if < 32 megbsMedia
db 0xF0
; Media DescriptorbsFatSize
dw 9
; Size of each FATbsTrackSect
dw 18
; Sectors per trackbsHeadCnt
dw 2
; number of read-write headsbsHidenSect
dd 0
; number of hidden sectorsbsHugeSect
dd 0
; if bsTotalSect is 0 this value is ; the number of sectorsbsBootDrv
db 0
; holds drive that the bs came frombsReserv
db 0
; not used for anythingbsBootSign
db 29h
; boot signature 29hbsVolID
dd 0
; Disk volume ID also used for temp ; sector # / # sectors to loadbsVoLabel
db "NO NAME " ; Volume LabelbsFSType
db "FAT12 " ; File System type <- FAT 12文件系統(tǒng);===========================================================================; Main start here;===========================================================================main:
cli ; 關(guān)閉可屏蔽中斷,以備我們接下來初始化寄存器的工作 mov ax,
cs ; 將代碼段傳給ax,實模式下,代碼段與數(shù)據(jù)段沒什么分別 mov ds,
ax ; 數(shù)據(jù)段寄存器,實際上都是0 mov es,
ax ; 附加段 mov ax,
ss ; 堆棧段 mov sp,0x7C00-1
; 堆棧指針,指向0x7BFF sti ; 基本工作已經(jīng)完成,開放中斷 ; mov ax,0x0003
int 0x10
mov si,msgHello
; 使 si指向 "Hello World!"字符串 call printStr
; 調(diào)用顯示子程序 mov si,fixLine
; 回車換行 call printStr
mov si,msgMore
; 顯示swithing to protect mode call printStr
; 打開A20門,幾乎所有想進入保護模式的程序都通過A20門來實現(xiàn) ; 當(dāng)然,也有其它辦法,比如 int 0x15,不過并不推薦,因為可能不兼容 ; 參考a20門以獲得更多細(xì)節(jié) cli call kbdwait
mov al,0xD1
out 0x64,
al call kbdwait
mov al,0xDF
out 0x60,
al call kbdwait
lgdt [__GDTR]
; 加載偽描述符到GDT高速寄存器 mov eax,
cr0 ; 將控制寄存器CR0的值放到eax中 or eax,1
; 置PE位 mov cr0,
eax ; 寫回CR0,這時候PE已經(jīng)被置位了 ; 進入保護模式的工作完成了一大半: ; 另一小半是:我們當(dāng)前的寄存器還在 ; 16位模式下工作 mov eax,osDataSel
; 初始化所有段寄存器 mov ds,
ax mov es,
ax mov ss,
ax mov fs,
ax mov gs,
ax mov esp,0x7C00-1
; 新的堆棧 jmp osCodeSel:code32
; 一個遠(yuǎn)跳轉(zhuǎn)以"沖"掉當(dāng)前實模式的代碼段CS及指令指針EIP ; 以使其使用保護模式的CS(注意:是選擇子),及EIP;------- END OF MAIN ----------------;===========================================================================; printStr; sub function for print a string to screen by INT 10H; 入口:es:si = 指向目標(biāo)字符串; 返回:無;===========================================================================printStr:
push si ; 保護寄存器 push ax push bx cld ; 清除進位標(biāo)志位,這個標(biāo)志位會影響 si 的遞增方向 mov ah,0x0E
; int 0x10 子功能號,顯示字符,參看相關(guān)資料以獲得細(xì)節(jié) mov bx,0x0007
; 頁號0,字符前景色 7,淺灰色,試著改變這個數(shù)值 ; 會給你的文字增添色彩 .nextChar:
lodsb ; [si] -> al,取一個字節(jié)碼 or al,
al ; 如果取得的字節(jié)是0,則表示字符串結(jié)束 jz .OK
; 退出 int 0x10
; 調(diào)用BIOS int 10h 中斷 jmp .nextChar
; 繼續(xù)下一個字符,直到遇到0 .OK:
pop bx ; 恢復(fù)寄存器 pop ax pop si ret ; 返回調(diào)用程序;------- END OF printStr --------------;=========================================================================; 等待鍵盤緩沖區(qū)清空kbdw0:
jmp short $+2
in al,0x60
kbdwait:
jmp short $+2
in al,0x64
test al,1
jnz kbdw0
test al,2
jnz kbdwait
ret;------ END OF kbdwait -----------------; data areamsgHello
db 'Hello World!',0
; 以物理 0結(jié)束msgMore
db 'Swithing to protect mode ...',0
fixLine
db 13,10,0
; 回車,換行的ASCII碼ALIGN 4
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; GDT 偽描述符; 參考保護模式相關(guān)文檔以獲得關(guān)于GDT偽描述; 符更詳細(xì)的資料;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;__GDTR:
dw GDT_END - GDT-1
; GDT表長度 dd GDT
; GDT物理地址;<- END OF __GDTR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; GDT entry; 參考保護模式相關(guān)文檔以獲得關(guān)于GDT的更; 詳細(xì)的資料;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ALIGN 8
; 對齊GDT:
; 第一個GDT作為保留項,以0填充; reserved GDT dd 0
dd 0
osCodeSel
equ $-GDT
; 內(nèi)核用的代碼段選擇子oscode:
dw 0xffff
dw 0
db 0
db 10011010b
; 0x9A ,可讀/可執(zhí)行 代碼段 db 11011111b
; db 0
osDataSel
equ $-GDT
; 內(nèi)核用的數(shù)據(jù)段選擇子osdata:
dw 0xffff
dw 0
db 0
db 10010010b
; 0x92 ,可讀/寫 數(shù)據(jù)段 db 11011111b
; db 0
GDT_END:
;<- END OF GDT;**************************************************************************; 32位代碼bits 32
; 告訴編譯器這段代碼在32位模式下工作code32:
; take a break nop ; 我不知道這三個NOP會不會起作用 nop nop ; 接下來讓我們直接向顯存寫數(shù)據(jù) mov [0xB8000+80*2*2],
BYTE 'P'
; 在屏幕的第三行第一列寫字母'P' mov [0xB8000+80*2*2+1],
BYTE 13
; 字母P的顏色為洋紅色 jmp $
bits 16
; 引導(dǎo)程序必須為512字節(jié),不用的地方以0填充 times 510-($-$$)
db 0
; $表示程序當(dāng)前位置,$$表示程序開始位置,由編譯器自動計算BOOT_SIGN
DW 0xAA55
; 最后兩個字節(jié)為引導(dǎo)標(biāo)志55AA