預備知識:
- 熟悉 i386 CPU寄存器,了解實模式及保護模式模式;
- 了解A20門
- 文本模式下直接顯存操作
涉及工具:
NASM,一個文本編輯器(我用的是ConText + NASM語法高亮),QEMU/VMWARE虛擬機
前言:
近期確實很忙,論壇里有一位朋友寫的代碼進入不了保護模式;我最初也是對保護模式相當敬畏,因為32位比16位要“復雜”的多;當時一直不敢下手,偶爾的嘗試有如蜻蜓點水,但最終以失敗告終。在學校的圖書館里幾乎找不到386保護模式匯編的資料,更不用說CPU相關的書了;不知道看了多少可憐的教材后,終于湊出了一點起眼的代碼,不過還是失敗了。我最終是通過一個Bona Fide 的實例教程解決了問題:實例程序在我的開發過程中起到了很重要的作用。
由于時間原因,這篇文章將主要以代碼來說明,因為我的確沒太多的時間再去介紹“實模式”,“保護模式”,GDT,IDT,A20等等等等相關的名詞、概念及規范;這些東西在我的網站里已經收羅了:http://www.xemean.net/resource/ ,其中有中文也有英文的,有的甚至是圖文并茂,網絡上也有不少的例子,但在這里強烈建議的一本電子教程是:《80X86保護模式教程》 ,這本書詳細地介紹了如何對 80X86 CPU進行編程,包括進入保護模式,保護模式的中斷,多任務等等。另外,值得一提的是:由楊季文等人編著的,清華大學出版社出版的《80x86匯編語言程序設計教程》也是一本不錯的書。
此文適合于有一定基礎,但又不能實現保護模式切換的朋友。
本文將以我上次寫的“啟動你的計算機”的代碼為基礎,演示進入保護模式,但程序還是在引導區內工作。
姑且不理會保護模式下“復雜”的內存管理,多任務,中斷,實際上進入保護只要:
mov eax,cr0 ; 控制寄存器CR0 -> EAX
or eax,1 ; 最低位置1,即PE位
mov cr0,eax ; 寫回CR0
I386兼容CPU使用CR0這個寄存器來“控制”或者說決定CPU的工作狀態,命名為“控制寄存器中”,其中CR0的最低位叫PE位,中文翻譯應該是“保護模式允許”位,如果對CR0的PE位置1,則CPU就工作在保護模式下。不幸的是,我們并不能直接對CR0進行操作,但是卻可以通過通用寄存器對其修改,上面便是開啟保護模式大門的實例。當然,只有上面的代碼你可能永遠也進不了保護模式。
保護模式與實模式有一個區別在于,段寄存器不再保存實際的內存地址,CPU已經有32位尋址的能力,也就是能訪問4G的內存,似乎用32位的EIP就可以訪問4G了,但Intel并沒有想得那么簡單,段寄存器在內存管理方面還有很大的作用。另外,之所以叫保護模式,是因為CPU還能不同應用層的代碼進行保護,這在16位實模式是做不到的。因此引入了GDT,及描述符的概念。(這就得請各位看官參看一些資料了)
CPU中有一個高速的寄存器用來保存GDT表在內存中的位置以及GDT表的大小:GDT的大小用16位來表示,GDT的物理地址用32位來表示(以保證GDT能在4G內存的任意位置),因此GDT高速寄存器(GDTR)占48位,已經不能用一個32位的寄存器來表示了,因此要在內存中表示出GDTR內容,書上說這叫“偽描述符”,GDTR由下面的指令裝載:
lgdt [__GDTR]
其中__GDTR是GDT偽描述符的地址,一口氣,我們作如下數據定義:
ALIGN 4
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; GDT 偽描述符
; 參考保護模式相關文檔以獲得關于GDT偽描述
; 符更詳細的資料
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
__GDTR:
dw GDT_END - GDT-1 ; GDT表的長度,由編譯器計算
dd GDT ; GDT物理地址,由編譯器計算
;<- END OF __GDTR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; GDT entry
; 參考保護模式相關文檔以獲得關于GDT的更
; 詳細的資料
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ALIGN 8 ; 對齊,以保護CPU訪問GDT的速度
GDT:
; 第一個GDT作為保留項,以0填充
; reserved GDT
dd 0
dd 0
osCodeSel equ $-GDT ; 內核用的代碼段選擇子
oscode:
dw 0xffff
dw 0
db 0
db 10011010b ; 0x9A ,可讀/可執行 代碼段
db 11011111b ;
db 0
osDataSel equ $-GDT ; 內核用的數據段選擇子
osdata:
dw 0xffff
dw 0
db 0
db 10010010b ; 0x92 ,可讀/寫 數據段
db 11011111b ;
db 0
GDT_END: ;<- END OF GDT
完成如上的定義之后,就可以著手進入保護模式了,過程大致為:
- 禁止所有中斷
- 打開A20門
- 加載GDTR
- 置PE位
- 初始化保護模式下的寄存器
- 一個遠跳轉到32位代碼以清除當前(實模式)的CS及EIP
如果跳轉成功,則CPU就可以工作在保護模式下。保護模式并不像實模式下有很多BIOS中斷可用,這就意味著我們必須自己寫鍵盤、顯卡等等驅動,計算機的幾乎所有資源都由內核來管理,當然,也由你來實現各種設備的驅動。
為了顯示我們的程序已經成功地工作在保護模式下,我們必須在32位模式時在屏幕上寫點什么東西,直接寫顯存吧!演示程序對顯存進行操作,結果是屏幕的第三行第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開始;===========================================================================; 程序執行的第一條指令必須是跳轉(如果你想使用FAT12這類文件系統的磁盤); 必須占用3字節;===========================================================================jmp SHORT main
; 2 bits,跳轉到主程序執行nop ; 1 bit;===========================================================================; FAT12 文件系統頭,從NYAOS 借過來的,可以參考相關的文檔以獲得更多細節; 這個塊會讓 Winimage 認出編譯后的二進制文件為有效的引導文件; 如果不使用這個塊,Winimage將不會將其作為引導程序處理; 但我們可以借助其它方法和工具處理,比如DEBUG;===========================================================================bsOEM
db "ExOS0.02" ; OEM String,任意你喜歡的8字節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文件系統;===========================================================================; Main start here;===========================================================================main:
cli ; 關閉可屏蔽中斷,以備我們接下來初始化寄存器的工作 mov ax,
cs ; 將代碼段傳給ax,實模式下,代碼段與數據段沒什么分別 mov ds,
ax ; 數據段寄存器,實際上都是0 mov es,
ax ; 附加段 mov ax,
ss ; 堆棧段 mov sp,0x7C00-1
; 堆棧指針,指向0x7BFF sti ; 基本工作已經完成,開放中斷 ; mov ax,0x0003
int 0x10
mov si,msgHello
; 使 si指向 "Hello World!"字符串 call printStr
; 調用顯示子程序 mov si,fixLine
; 回車換行 call printStr
mov si,msgMore
; 顯示swithing to protect mode call printStr
; 打開A20門,幾乎所有想進入保護模式的程序都通過A20門來實現 ; 當然,也有其它辦法,比如 int 0x15,不過并不推薦,因為可能不兼容 ; 參考a20門以獲得更多細節 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已經被置位了 ; 進入保護模式的工作完成了一大半: ; 另一小半是:我們當前的寄存器還在 ; 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
; 一個遠跳轉以"沖"掉當前實模式的代碼段CS及指令指針EIP ; 以使其使用保護模式的CS(注意:是選擇子),及EIP;------- END OF MAIN ----------------;===========================================================================; printStr; sub function for print a string to screen by INT 10H; 入口:es:si = 指向目標字符串; 返回:無;===========================================================================printStr:
push si ; 保護寄存器 push ax push bx cld ; 清除進位標志位,這個標志位會影響 si 的遞增方向 mov ah,0x0E
; int 0x10 子功能號,顯示字符,參看相關資料以獲得細節 mov bx,0x0007
; 頁號0,字符前景色 7,淺灰色,試著改變這個數值 ; 會給你的文字增添色彩 .nextChar:
lodsb ; [si] -> al,取一個字節碼 or al,
al ; 如果取得的字節是0,則表示字符串結束 jz .OK
; 退出 int 0x10
; 調用BIOS int 10h 中斷 jmp .nextChar
; 繼續下一個字符,直到遇到0 .OK:
pop bx ; 恢復寄存器 pop ax pop si ret ; 返回調用程序;------- END OF printStr --------------;=========================================================================; 等待鍵盤緩沖區清空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結束msgMore
db 'Swithing to protect mode ...',0
fixLine
db 13,10,0
; 回車,換行的ASCII碼ALIGN 4
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; GDT 偽描述符; 參考保護模式相關文檔以獲得關于GDT偽描述; 符更詳細的資料;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;__GDTR:
dw GDT_END - GDT-1
; GDT表長度 dd GDT
; GDT物理地址;<- END OF __GDTR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; GDT entry; 參考保護模式相關文檔以獲得關于GDT的更; 詳細的資料;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ALIGN 8
; 對齊GDT:
; 第一個GDT作為保留項,以0填充; reserved GDT dd 0
dd 0
osCodeSel
equ $-GDT
; 內核用的代碼段選擇子oscode:
dw 0xffff
dw 0
db 0
db 10011010b
; 0x9A ,可讀/可執行 代碼段 db 11011111b
; db 0
osDataSel
equ $-GDT
; 內核用的數據段選擇子osdata:
dw 0xffff
dw 0
db 0
db 10010010b
; 0x92 ,可讀/寫 數據段 db 11011111b
; db 0
GDT_END:
;<- END OF GDT;**************************************************************************; 32位代碼bits 32
; 告訴編譯器這段代碼在32位模式下工作code32:
; take a break nop ; 我不知道這三個NOP會不會起作用 nop nop ; 接下來讓我們直接向顯存寫數據 mov [0xB8000+80*2*2],
BYTE 'P'
; 在屏幕的第三行第一列寫字母'P' mov [0xB8000+80*2*2+1],
BYTE 13
; 字母P的顏色為洋紅色 jmp $
bits 16
; 引導程序必須為512字節,不用的地方以0填充 times 510-($-$$)
db 0
; $表示程序當前位置,$$表示程序開始位置,由編譯器自動計算BOOT_SIGN
DW 0xAA55
; 最后兩個字節為引導標志55AA