codefans

导航

<2024年12月>
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234

统计

常用链接

留言簿(2)

随笔分类

随笔档案

文章分类

文章档案

程序设计链接

搜索

最新评论

阅读排行榜

评论排行榜

开发教程][原创] 操作系统DIY - 进入保护模式

预备知识:

  1. 熟悉 i386 CPU寄存器,了解实模式及保护模式模式;
  2. 了解A20门
  3. 文本模式下直接显存操作

涉及工具:
  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

完成如上的定义之后,就可以着手进入保护模式了,过程大致为:

  1. 禁止所有中断
  2. 打开A20门
  3. 加载GDTR
  4. 置PE位
  5. 初始化保护模式下的寄存器
  6. 一个远跳转到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 sector
bsClustSize db 1                        ; Sectors per cluster
bsRessect   dw 1                        ; # of reserved sectors
bsFatCnt    db 2                        ; # of fat copies
bsRootSize  dw 224                      ; size of root directory
bsTotalSect dw 2880                     ; total # of sectors if < 32 meg
bsMedia     db 0xF0                     ; Media Descriptor
bsFatSize   dw 9                        ; Size of each FAT
bsTrackSect dw 18                       ; Sectors per track
bsHeadCnt   dw 2                        ; number of read-write heads
bsHidenSect dd 0                        ; number of hidden sectors
bsHugeSect  dd 0                        ; if bsTotalSect is 0 this value is
                                        ; the number of sectors
bsBootDrv   db 0                        ; holds drive that the bs came from
bsReserv    db 0                        ; not used for anything
bsBootSign  db 29h                      ; boot signature 29h
bsVolID     dd 0                        ; Disk volume ID also used for temp
                                        ; sector # / # sectors to load
bsVoLabel   db "NO NAME    "            ; Volume Label
bsFSType    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 area

msgHello 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

posted on 2005-09-14 00:23 春雷的博客 阅读(528) 评论(0)  编辑  收藏


只有注册用户登录后才能发表评论。


网站导航: