自己动手写操作系统(三)

作者:伊梅 本文选自:开放系统世界——赛迪网 2002年12月04日 http://developer.ccidnet.com/pub/disp/Article?columnID=322&articleID=32660&pageNO=1

在上两期中( 自己动手写操作系统12),我向大家讲述了如何使用Linux提供的开发工具在软盘的启动扇区写一些代码,以及如何调用BIOS的问题。现在,这个操作系统已经越来越接近当年Linus Torvalds的那个具有“历史意义”的Linux内核了。因此,要马上把这个系统切换到保护模式之下。

什么是保护模式


自从1969年推出第一个微处理器以来,Intel处理器就在不断地更新换代,从8086、8088、80286,到80386、80486、奔腾、奔腾Ⅱ、奔腾4等,其体系结构也在不断变化。80386以后,提供了一些新的功能,弥补了8086的一些缺陷。这其中包括内存保护、多任务及使用640KB以上的内存等,并仍然保持和8086家族的兼容性。也就是说80386仍然具备了8086和80286的所有功能,但是在功能上有了很大的增强。早期的处理器是工作在实模式之下的,80286以后引入了保护模式,而在80386以后保护模式又进行了很大的改进。在80386中,保护模式为程序员提供了更好的保护,提供了更多的内存。事实上,保护模式的目的不是为了保护程序,而是要保护程序以外的所有程序(包括操作系统)。

简言之,保护模式是处理器的一种最自然的模式。在这种模式下,处理器的所有指令及体系结构的所有特色都是可用的,并且能够达到最高的性能。

保护模式和实模式


从表面上看,保护模式和实模式并没有太大的区别,二者都使用了内存段、中断和设备驱动来处理硬件,但二者有很多不同之处。我们知道,在实模式中内存被划分成段,每个段的大小为64KB,而这样的段地址可以用16位来表示。内存段的处理是通过和段寄存器相关联的内部机制来处理的,这些段寄存器(CS、DS、SS和ES)的内容形成了物理地址的一部分。具体来说,最终的物理地址是由16位的段地址和16位的段内偏移地址组成的。用公式表示为:

物理地址=左移4位的段地址+偏移地址。

在保护模式下,段是通过一系列被称之为“描述符表”的表所定义的。段寄存器存储的是指向这些表的指针。用于定义内存段的表有两种:全局描述符表(GDT)和局部描述符表(LDT)。GDT是一个段描述符数组,其中包含所有应用程序都可以使用的基本描述符。在实模式中,段长是固定的(为64KB),而在保护模式中,段长是可变的,其最大可达4GB。LDT也是段描述符的一个数组。与GDT不同,LDT是一个段,其中存放的是局部的、不需要全局共享的段描述符。每一个操作系统都必须定义一个GDT,而每一个正在运行的任务都会有一个相应的LDT。每一个描述符的长度是8个字节,格式如图3所示。当段寄存器被加载的时候,段基地址就会从相应的表入口获得。描述符的内容会被存储在一个程序员不可见的影像寄存器(shadow register)之中,以便下一次同一个段可以使用该信息而不用每次都到表中提取。物理地址由16位或者32位的偏移加上影像寄存器中的基址组成。实模式和保护模式的不同可以从图1和图2中很清楚地看出来。

此外,还有一个中断描述符表(IDT)。这些中断描述符会告诉处理器到那里可以找到中断处理程序。和实模式一样,每一个中断都有一个入口,但是这些入口的格式却完全不同。因为在切换到保护模式的过程中没有使用到IDT,所以在此就不多做介绍了。

进入保护模式


80386有4个32位控制寄存器,名字分别为CR0、CR1、CR2和CR3。CR1是保留在未来处理器中使用的,在80386中没有定义。CR0包含系统的控制标志,用于控制处理器的操作模式和状态。CR2和CR3是用于控制分页机制的。在此,我们关注的是CR0寄存器的PE位控制,它负责实模式和保护模式之间的切换。当PE=1时,说明处理器运行于保护模式之下,其采用的段机制和前面所述的相应内容对应。如果PE=0,那么处理器就工作在实模式之下。

切换到保护模式,实际就是把PE位置为1。为了把系统切换到保护模式,还要做一些其它的事情。程序必须要对系统的段寄存器和控制寄存器进行初始化。把PE位置1后,还要执行跳转指令。过程简述如下:

1.创建GDT表;

2.通过置PE位为1进入保护模式;

3.执行跳转以清除在实模式下读取的任何指令。

下面使用代码来实现这个切换过程。

需要的东西


◆ 一张空白软盘

◆ NASM编译器

下面是整个程序的源代码:

      
      
       
       org 0x07c00; 起始地址是0000:7c00 
jmp short begin_boot   ; 跳过其它的数据,跳转到引导程序的开始处
bootmesg db "Our OS boot sector loading ......"
pm_mesg  db "Switching to protected mode ...."
dw 512  ; 每一扇区的字节数
db 1  ; 每一簇的扇区数
dw 1  ; 保留的扇区号
db 2		    
dw 0x00e0	    
dw 0x0b40 	    
db 0x0f0	    
dw 9		    
dw 18		    
dw 2  ; 读写扇区号
dw 0  ; 隐藏扇区号
print_mesg :
   mov ah,0x13  ; 使用中断10h的功能13,在屏幕上写一个字符串
   mov al,0x00  ; 决定调用函数后光标所处的位置
   mov bx,0x0007    ; 设置显示属性
   mov cx,0x20  ; 在此字符串长度为32 
   mov dx,0x0000    ; 光标的起始行和列
   int 0x10  ; 调用BIOS的中断10h
   ret  ; 返回调用程序
get_key :
   mov ah,0x00	  
   int 0x16  ; Get_key使用中断16h的功能0,读取下一个字符
   ret
clrscr :
   mov ax,0x0600  ; 使用中断10h的功能6,实现卷屏,如果al=0则清屏
   mov cx,0x0000  ; 清屏
   mov dx,0x174f  ; 卷屏至23,79
   mov bh,0  ; 使用颜色0来填充
   int 0x10  ; 调用10h中断
   ret
begin_boot :
   call clrscr   ; 先清屏
   mov bp,bootmesg  ; 提供串地址
   call print_mesg  ; 输出信息
   call get_key	  ; 等待用户按下任一键
bits 16
   call clrscr  ; 清屏
   mov ax,0xb800  ; 使gs指向显示内存
   mov gs,ax  ; 在实模式下显示一个棕色的A
   mov word [gs:0],0x641  ; 显示
   call get_key  ; 调用Get_key等待用户按下任一键
   mov bp,pm_mesg  ;  设置串指针
   call print_mesg  ; 调用print_mesg子程序
   call get_key  ; 等待按键
   call clrscr  ; 清屏
   cli  ; 关中断
   lgdt[gdtr]  ; 加载GDT	
   mov eax,cr0	    
   or al,0x01  ; 设置保护模式位
   mov cr0,eax  ; 将更改后的字送至控制寄存器中
   jmp codesel:go_pm
bits 32
go_pm : 
   mov ax,datasel   
   mov ds,ax  ; 初始化ds和es,使其指向数据段
   mov es,ax	
   mov ax,videosel  ; 初始化gs,使其指向显示内存
   mov gs,ax	
   mov word [gs:0],0x741 ; 在保护模式下显示一个白色的字符A
spin : jmp spin  ; 循环
bits 16
gdtr :
   dw gdt_end-gdt-1  ; gdt的长度
   dd gdt  ; gdt的物理地址
gdt
nullsel equ $-gdt  ; $指向当前位置,所以nullsel = 0h
gdt0  ; 空描述符
   dd 0		       
   dd 0  ; 所有的段描述符都是64位的
codesel equ $-gdt  ; 这是8h也就是gdt的第二个描述符
code_gdt		       
   dw 0x0ffff  ; 段描述符的界限是4Gb
   dw 0x0000	      
   db 0x00            
   db 0x09a	   
   db 0x0cf	   
   db 0x00	   
datasel equ $-gdt	   
data_gdt		       
   dw 0x0ffff	       
   dw 0x0000	       
   db 0x00	           
   db 0x092
   db 0x0cf
   db 0x00
videosel equ $-gdt	    
   dw 3999	       
   dw 0x8000  ; 基址是0xb8000
   db 0x0b
   db 0x92	       
   db 0x00	       
   db 0x00
gdt_end
times 510-($-$$)  db 0  
dw 0x0aa55
      
      


把上面的代码存在一个名为abc.asm的文件之中,使用命令nasm abc.asm,将得出一个名为abc的文件。然后插入软盘,输入命令:dd if=abc of=/dev/fd0。该命令将把文件abc写入到软盘的第一扇区之中。然后重新启动系统,就会看到如下的信息:

      
      
       
       *Our os booting................ 
* A (棕色) 
* Switching to protected mode.... 
* A (白色)
      
      


对代码的解释


上面给出了所有的代码,下面我对上述代码做一些解释。

◆ 使用的函数

下面是代码中一些函数的说明:

print_mesg 该子程序使用了BIOS中断10h的功能13h,即向屏幕写一字符串。属性控制是通过向一些寄存器中送入不同的值来实现的。中断10h是用于各种字符串操作,我们把子功能号13h送到ah中,用于指明要打印一个字符串。al寄存器中的0说明了光标返回的起始位置,0表示调用函数后光标返回到下一行的行首。如果al为1则表示光标位于最后一个字符处。

显存被分成了几页,在同一时刻只能显示其中的一页。bh指明的是页号;bl则指明要显示字符的颜色;cx指明要显示字符串的长度;dx指明光标的位置(即起始的行和列)。所有相关寄存器初始化完成以后,就可以调用BIOS中断10h了。

get_key 使用中断16h的子功能00h,从屏幕得到下一个字符。

clrscr 该函数使用了中断10h的另外一个子功能06h,用于输出开始前清屏。初始化时给al中送入0。寄存器cx和dx指明要清屏的屏幕范围,在本例中是整个屏幕。寄存器bh指明屏幕填充的颜色,在本例中是黑色。

◆ 其它内容

程序一开始是一条短跳转指令,跳到begin_boot处。在实模式下,在此打印一个棕色的“A”,并且设置一个GDT。切换到保护模式,并且打印一个白色的“A”。这两种模式使用的都是自己的寻址方法。

在实模式下,使用段寄存器gs指示显存位置,我们使用的是CGA显卡(默认基址是0xb8000)。在代码中是不是漏了一个0呢?没有,因为实模式下会提供一个附加的0。这种方式也被80386继承下来了。A的ASCⅡ是0x41,0x06指明了需要一个棕色的字符。该显示会一直持续直至按下任意键。下面要在屏幕上显示一句话,告诉使用者下面马上要进入保护模式了。

启动到保护模式,在进行切换时不希望此时有中断的影响,故要关闭所有的中断(使用cli来实现)。然后对GDT初始化。在整个切换过程中,对4个描述符进行了初始化。这些描述符对代码段(code_gdt)、数据和堆栈段(data_gdt),以及为了访问显存而对显示段进行初始化。此外,还会对一个空描述符进行初始化。

GDT的基址要加载至GDTR系统寄存器之中。gdtr段的第一个字加载的是GDT的大小,在下一个双字中则加载的是基址。然后,lgdt指令把把gdt段加载至GDTR寄存器中。现在已经做好了切换到保护模式前的所有准备。最后一件事情就是把CR0寄存器的PE位置1。不过,即使这样还没有处于保护模式状态之下。

设置了PE位以后,还需要通过执行JMP指令来清除处理器指令预取队列。在80386中,使用指令前总是先将其从内存中取出,并且进行解码和寻址。然而,当进入保护模式以后,预取指令信息(它还处于实地址模式)就无效了。使用JMP指令的目的就是强迫处理器放弃无效的信息。

现在,已经在保护模式下了。那么,如何检测是在保护模式状态之下呢?让我们来看一看屏幕上这个白色的字母A。在这里,使用了数据段选择符(datase1)对数据段和附加段进行了初始化,使用显示段选择符(videose1)对gs进行了初始化。告示的字符“A”其ASCⅡ值和属性位于[gs:0000]处,也就是b8000:0000处。循环语句使得该字符一直在屏幕上显示,直至重新启动系统。

下一步要做的事


现在,这个操作系统已经工作在保护模式下了,但是实际上它并不实现什么具体的功能。你可以在这个基础上为它增加各种操作系统所具有的功能。我们自己动手写操作系统到此也就告一段落。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本书在详细分析操作系统原理的基础上,用丰富的实例代码,一步一步地指导读者用C语言和汇编语言编出一个具备操作系统基本功能的操作系统框架。本书不同于其他的理论型书籍,而是提供给读者一个动手实践的路线图。书中讲解了大量在开发操作系统中需注意的细节问题,这些细节不仅能使读者更深刻地认识操作系统的核心原理,而且使整个开发过程少走弯路。全书共分7章。 本书适合各类程序员、程序开发爱好者阅读,也可作为高等院校操作系统课程的实践参考书。 折叠 作品目录 第1章 马上动手一个最小的"操作系统"1 1.1 准备工作1 1.2 10分钟完成的操作系统1 1.3 Boot Sector3 1.4 代码解释3 1.5 水面下的冰山5 1.6 回顾6 第2章 搭建你的工作环境7 2.1 虚拟计算机(Virtual PC)7 2.1.1 Virtual PC初体验8 2.1.2 创建你的第一个Virtual PC9 2.1.3 虚拟软盘研究12 2.1.4 虚拟软盘实战14 2.2 编译器(NASM & GCC)18 2.3 安装虚拟Linux19 2.4 在虚拟Linux上访问Windows文件夹26 2.5 安装虚拟PCDOS26 2.6 其他要素29 2.7 Bochs29 2.7.1 Bochs vs. Virtual PC vs. VMware30 2.7.2 Bochs的使用方法31 2.7.3 用Bochs进行调试33 2.7.4 在Linux上开发34 2.8 总结与回顾36 第3章 保护模式(Protect Mode)37 3.1 认识保护模式37 3.1.1 GDT(Global Descriptor Table) 42 3.1.2 实模式到保护模式,不一般的jmp45 3.1.3 描述符属性47 3.2 保护模式进阶50 3.2.1 海阔凭鱼跃50 3.2.2 LDT(Local Descriptor Table)58 3.2.3 特权级62 3.3 页式存储82 3.3.1 分页机制概述83 3.3.2 编代码启动分页机制84 3.3.3 PDE和PTE85 3.3.4 cr388 3.3.5 回头看代码88 3.3.6 克勤克俭用内存90 3.3.7 进一步体会分页机制100 3.4 中断和异常107 3.4.1 中断和异常机制109 3.4.2 外部中断111 3.4.3 编程操作8259A113 3.4.4 建立IDT116 3.4.5 实现一个中断117 3.4.6 时钟中断试验119 3.4.7 几点额外说明121 3.5 保护模式下的I/O122 3.5.1 IOPL122 3.5.2 I/O许可位图(I/O Permission Bitmap)123 3.6 保护模式小结123 第4章 让操作系统走进保护模式125 4.1 突破512字节的限制125 4.1.1 FAT12126 4.1.2 DOS可以识别的引导盘131 4.1.3 一个最简单的Loader132 4.1.4 加载Loader入内存133 4.1.5 向Loader交出控制权142 4.1.6 整理boot.asm142 4.2 保护模式下的"操作系统"144 第5章 内核雏形146 5.1 用NASM在LinuxHello World146 5.2 再进一步,汇编和C同步使用148 5.3 ELF(Executable and Linkable Format)150 5.4 从Loader到内核155 5.4.1 用Loader加载ELF155 5.4.2 跳入保护模式161 5.4.3 重新放置内核170 5.4.4 向内核交出控制权175 5.4.5 操作系统的调试方法176 5.5 扩充内核184 5.5.1 切换堆栈和GDT184 5.5.2 整理我们的文件夹191 5.5.3 Makefile191 5.5.4 添加中断处理200 5.5.5 两点说明218 5.6 小结219 第6章 进程221 6.1 迟到的进程221 6.2 概述222 6.2.1 进程介绍222 6.2.2 未雨绸缪--形成进程的必要考虑222 6.2.3 参考的代码224 6.3 最简单的进程224 6.3.1 简单进程的关键技术预测225 6.3.2 第一步--ring0→ring1227 6.3.3 第二步--丰富中断处理程序243 6.3.4 进程体设计技巧254 6.4 多进程256 6.4.1 添加一个进程体256 6.4.2 相关的变量和宏257 6.4.3 进程表初始化代码扩充258 6.4.4 LDT260 6.4.5 修改中断处理程序261 6.4.6 添加一个任务的步骤总结263 6.4.7 号外:Minix的中断处理265 6.4.8 代码回顾与整理269 6.5 系统调用280 6.5.1 实现一个简单的系统调用280 6.5.2 get_ticks的应用286 6.6 进程调度292 6.6.1 避免对称--进程的节奏感292 6.6.2 优先级调度总结300 第7章 输入/输出系统302 7.1 键盘302 7.1.1 从中断开始--键盘初体验302 7.1.2 AT、PS/2键盘304 7.1.3 键盘敲击的过程304 7.1.4 解析扫描码309 7.2 显示器325 7.2.1 初识TTY325 7.2.2 基本概念326 7.2.3 寄存器328 7.3 TTY任务332 7.3.1 TTY任务框架的搭建334 7.3.2 多控制台340 7.3.3 完善键盘处理346 7.3.4 TTY任务总结354 7.4 区分任务和用户进程354 7.5 printf357 7.5.1 为进程指定TTY357 7.5.2 printf()的实现358 7.5.3 系统调用write()361 7.5.4 使用printf()363 后记366

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值