预备知识:
- 熟悉 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