一篇自己动手写操作系统(转贴)

哈工大纯C论坛
操作系统实验
When Do We Write Our Chinese OS ?

操作系统是计算机的核心,没有操作系统,一切计算机应用都免谈,但现在操作系统基本上被老外垄断,Windows就不说了,就算是Linux那也是泊来品,什么时候我们才能写出有中国特色的操作系统啊?

在工大,我们每个人都学过操作系统,我也一样,但老师教的那真的只是理论,一个实际的系统原比老师教的要复杂上一千万倍!然而,我们基本上没有可能实践的机会,就算是有一些实验,那也是停留在一个非常高的表层。我非常之笨,学完之后,还是不知系统是怎样从无到有,开始工作的。系统是怎样启动的?曾经把一段汇编代码写进了磁盘,但无论如何没将机器启动起来(原因现在已经清楚,稍后再谈)。想看看Linux的源码,但犹如天书!即使是Minix也非常之庞大,晦涉难懂!郁闷啊~~~,今天在网上无意间看见了一个老外写的E文,讲到此事,霍然开朗,在网上狂找了一堆E文后,最终将机器起启来了。(本人E文差得没底,基本上是用金山词霸配合联通国际在线翻译系统一句一句翻译的,痛苦啊!看来要想学好计算机E文不好还真不行:(,很多资料中文的就是没有,只有E文的,你咋办?)。现特将全过程描述一下,一来留个纪念,二来希望工大能有更多的人能对此感兴趣,超级大牛们早日写出我们自己的操作系统。Linux也是芬兰的一个大学生写的。

阅读本文最好有那么一丁点的汇编基础。另外,本人水平极其有限,对操作系统也不是很熟,对于论述中不计其数的错误,望大家原谅,千万不要来砍我~~~
首先,我要先说明一下计算机在你按下电源按钮后,计算机都做了什么
当你按下电源键的时候,同这个键相联的电线就会送出一个电信号给主板,主板将此电信号传给供电系统,供电系统开始工作,为整个系统供电,并送出一个电信号给BIOS,通知BIOS供电系统已经准备完毕。随后BIOS启动一个程序,进行主机自检,主机自检主要工作是确保系统的每一个部分都得到了电源支持,内存储器,主板上的其它芯片,键盘,鼠标,磁盘控制器及一些I/O端口正常可用,此后,自检程序将控制权还给BIOS。

接下来,BIOS开始启动操作系统。
BIOS将访问启动盘的第一个扇区(0磁道,1扇区,一共是512字节),BIOS将这第一扇区中的内容调入内存的0x7c00地址处,并开始执行它。这是启动系统的第一关,从此之后,系统就将控制权将给操作系统了,留下的事情就由你的程序来完成。

现在我们的任务就是写这样一个程序,系统将它称之为引导程序,用它来引导或说启动我们的计算机。它有如下两个特点:
1。大小只能是512字节,不能多一字节,也不能少一字节。因为BIOS只能读512字节的数据到内存中,多的部份BIOS不会理采
2。它必须以 "55 AA "结尾,即最后两字节(511,512)必须是它们。这是引导区程序结束的标志,没有它BIOS不会将它作为引导程序看待。(我以前的程序没有执行,就是因为没有在这里写 "55 AA ")
把这一程序放在磁盘的0磁道,1扇区里,这样,此磁盘就可以用来引导系统,而且是用的你自己做的引导程序!

在开始制作引导程序之前,先介绍一下怎样在Windows环境下进行这样的开发。

首先,需要一个实验环境,你当然可以就用真实的计算机,如果你有多台计算机的话,且不觉得麻烦的话。
这里我们使用虚拟机来进行实验,它与使用真实的计算机是一样的,不信待会儿你可以自己实验一下。
我用的是 MS Virtual PC,使用非常简单,这里就不多说了,它可以用一个1.44M大小的img文件,作为模拟软盘,因此,我们就只需把我们的引导程序写到一个img文件中,就如同写在了一张磁盘上面,就可以用它来引导系统。
启动虚拟机后,在Floopy菜单下,选Floopy Disk Image项,然后选到我们生成的那个img文件后,就可以了。

下面说说img文件的创建生成方法
要把引导程序写到这个1.44M的文件里面,我使用的是WinHex工具,它非常方便,可以直接通过拷贝完成二进制文件的写入,而且还可以创建指定大小的文件。
1.44M的img文件可以用WinHex来创建,点击新建按钮就行,大小输入1474560,单位字节。
到时后,把我们写的程序用WinHex打开,将其内容复制到生成的img文件下就行。

这些工具连同本文所介绍的实验程序,我已经打包,大家可以通过下面的地址下载
ftp://202.118.239.46/Incoming/Other/BTC/temp/os_test/os_test.rar

下面再介绍一个本程序用到的唯一一个BIOS中断,
Int 0x10
0x10 中断是BIOS的显示器中断,所有输出都需要调用此中断,在使用前你需要设置一部分寄存器的值以告诉BIOS怎样进行输出
ah : 0x0e 打字机模式,告诉BIOS,把字符输出到屏幕上
bh : 页码
bl : 文字属性
al : 欲现示的字符的ASCII码

好了,下面我们就能创建我们的引导程序,完整的源程序如下:

[BITS 16] ; 告诉编译器,编译成16位的程序
[ORG 0x7C00] ; 告诉编译器,代码将从0x7c00处开始执行

main: ; 主程序
mov ax , 0x0000 ; 以下两句设置数据段为0000
mov ds , ax
mov si , Message ; 设置基址指针
call ShowMessage ; 调用显示函数
jmp $ ; $ 代表此语句的地址,表示在此语句此进行无限循环

ShowMessage: ; 显示函数
mov ah , 0x0e ; 设置现示模式
mov bh , 0x00 ; 设置页码
mov bl , 0x07 ; 设置字体属性

nextchar:
lodsb ; 字符载入指令
; 它将DS数据段中SI为偏移地址的源串中的一个字符取出送AL,同时修改SI指向下一个字符
or al , al ; 测试字符串是否为0
jz return ; 如果为零则表明字符串结束,跳转到返回指令处返回原调用函数
int 0x10 ; 调用BIOS 10号中断显示字符
jmp nextchar ; 继续显示下一下字符
return:
ret ; 返回原调用函数

Message:
db 'Can We Write A Chinese OS ? '; 定义显示消息
db 13 , 10 , 0 ; 13 表示回车,10 表示换到下一行 0 表示字符串结束

times 510 - ( $ - $$ ) db 0 ; 填充 0 以满足文件大小足够512字节
; $ 表示当前语句的地址,$$ 表示程序的起始地址
db 0x55 , 0xaa ; 结束标志

用任何一个文件编辑器输入它之后,存为test.asm
然后用一个汇编程序编译它,我用的是nasm,
输入 nasm test.asm
编译完成后,会生成一个 test 文件,用WinHex打开它,然后照上面讲的方法,把它写到一个img文件中,你就可以尝试一下用你自己的程序启动计算机了。
也可以用WinHex将它直接写到磁盘上,通过磁盘启动,这样的感觉非常爽啊~~~

下面是它的运行结果

此主题相关图片如下:

一个操作系统非常的复杂,这里只是我的一个尝试实验的心得,它只完成了系统启动这一步,严格说来引导程序不算是操作系统,虽然它无比重要,一般来说,它需要把真正的操作系统的内核载入内存,然后用一条jmp指令跳转到真正的内核处执行。

上一篇谈到了怎样把计算机用我们自己的程序启动起来,然而我已经说过,那只是最最初
始的一步,只表明了我们的确可以让计算机从一开始就按照我们的命令去执行一个任务。
但它并不能算是一个操作系统,当计算机用引导程序引导起来之后,我们需要让它把真正
的操作系统内核载入内存中,然后跳转到真正的操作系统内核中运行。
本篇将完成一个真正意义上的操作系统引导,而不是第一篇里所描述的计算机的引导。这
里我们将从计算机启动时的16位的实模式转到现在通用的32位的保护模式下。
现在的操作系统除了最低层的部份之外,均由高级语言完成,在本篇中我们也将用高级语
言来编写我们的内核。本篇中实现的一个内核是用C语言编写的。用汇编写的引导程序,把
C语言写的内核载入并执行,这就是本篇将要完成的任务。
阅读本文最好有那么一丁点的汇编基础~~~
还是那句老话,我水平极其有限,这只是一个尝试,对于不计其数的错误,望大家海涵,
千万不要来砍我啊~~~
非常感谢各位老师,大牛能指点一二!
现在,我们继续我们的实验。
首先,我想先介绍一下实模式与保护模式,对于本篇,它们是非常重要的概念!一定要领
会!就算现在领会有误也要尽可能的领会!大家都知道现在用的Intel的CPU在原先8086的
时代是16位的,后来技术发展了,Intel的CPU也改成了32位的了。但是为了同以前16位的
程序兼容,Inetl在它的32位的CPU中仍然保留了16位的模式。这样在32位的CPU中,就有两
种工作模式,一种是16位的模式,称之为实模式;另一种是32位的模式,称之为保护模式
。在计算机刚刚启动的时候,它是工作在实模式情况下,为了让它工作在保护模式的情况
下,我们需要向CPU中一个cr0的寄存器置位,这样,当cr0的第0位被置位后,CPU就转到32
位模式下工作了。因此,从16位转到32位是非常简单的,只需要对CPU的一个寄存器进行置
位操作就行了。但是,还是需要做一个准备工作,这里我们先来谈谈16位与32位模式下内
存是怎样访问的。
学过汇编的都知道,在8086中,内存是分段的,一个内存地址由 段基址:偏移量 构成。这
是因为在16位的CPU中,寄存器的大小是16位的,只能访问2^16=64K的地址,这实在是太小
了,因此需要通过分段的机制来扩大程序的访问内存的范围。显然,在16位的CPU中,一个
段的大小最大只能是64K
但是在32位的CPU中,寄存器的大小是32位的,可以访问2^32=4GB有空间,这又太大了!一
个程序只需要其中的能小一部分空间就足够了。因此,在32位的CPU中,也将内存分段,为
每个程序分配必要的空间,以免浪费。(注,在32位的CPU中,内存管理有分段与分页两种
模式,分页主要用于实现虚拟内存,本文只讨论分段模式)这样就需要记住每个段是属于
哪个程序的?是用来干啥的?是数据段还是代码段?可以访问还是不可以访问?是只读的
还是可写的?。。。这些需要记录的信息是在太多了,CPU就把它们组织起来放在内存中一
个专门的地方,这块内存就称之为段描述符,它描述了这个段的属性。每个段都有一个描
述属性的段描述符,把它们全部组织起来放在内存的一个专门的地方,就形成了段描述符
表。32位CPU有全局描述符表(GDT)与局部描述符表(LDT)之分。全局描述符表就指此表
可被所有的程序访问。在32位的CPU中,段寄存器并不是表示段的基址,而是表示一个指向
描述符表的索引,用来指出此程序用到的段的描述符在描述符表的位置。然后,访问此描
述符表取出描述符,由此才得到有关段的属性信息。32位CPU的段寄存器器还是16位的,不
过,它用13位来作段描述符表的索引,因此,我们可以定义的最大段数为2^13=8192个。由
此可见,在32位的保护模式下,内存是通过段描述符表来访问的,因此,当我们转到32位
模式之间,我们需要创建好段与段描述符表。
好了,基础知识我们已经有了,闲话少说,我们来编写我们的引导程序,完整的源代码如
下:

 

注:在上一篇已经注解过的地方,这次就不注解了,如果有不理解之处,请找上一篇看看
[BITS 16]
[ORG 0x7C00]
jmp main
; ---------------------------------------------------------------------------
; 数据定义
bootdrive db 0
; --------------------------------------------------------------
; GDT 定义,此处定义段及段描述符
gdt:
gdt_null: ; 这是CPU要求保留的,CPU要求第一个段必须是空段,不知它
; 想用来干啥
dd 0
dd 0 ; 每个段的描述符是64位(8字节),空描述符的这64位全是0
gdt_code_addr equ $ - gdt ; 求得代码段在GDT表中的位置
gdt_code:
dw 0xffff ; 段大小为4GB
dw 0 ; 基地址(24位)
db 0
db 10011010b ; 属性描述位,指明此是代码段,可读可执行
db 11001111b ; 具体的每一位是代表什么可查Intel CPU编程手册
db 0 ; 没有的可以去网上下,也可以找我要
gdt_data_addr equ $ - gdt ; 求得数据段在GDT表中的位置
gdt_data:
dw 0xffff
dw 0
db 0
db 10010010b ; 指明此是数据段,可读可写
db 11001111b
db 0
gdt_end:
gdt_addr:
dw gdt_end - gdt - 1 ; GDT 表的大小
dd gdt ; GDT 表的位置
; ---------------------------------------------------------------------------
main: ; 引导程序从此处开始执行
mov [bootdrive] , dl ; 得到启动的驱动器号
xor ax , ax ; 设置 DS
mov ds , ax
; 清屏
;mov ax , 3 ; 设置清屏功能号
;int 0x10 ; 调用 BIOS 10 号中断清屏
.ResetFloppy ; 重置磁盘,不是必须的,主要是为了安全起见
mov ax , 0 ; 设置重置磁盘的功能号
mov dl , [bootdrive] ; 选择启动磁盘
int 0x13 ; 调用 BIOS 13 号中断重置磁盘
jc .ResetFloppy ; 如果出错则重试
.ReadFloppy ; 读内核到内存中 0000:9000 (es:bx)处,
; 为什么要读到9000处,这不是一定的,
; 你可以读到另外一个合适的地址
; 什么地址合适?怎样知道一个地址合不合适?待会再说
xor ax , ax ; 设置 es 寄存器
mov es , ax
mov bx , 0x9000
mov ah , 2 ; 设置读磁盘功能号
mov dl , [bootdrive] ; 设置欲读驱动器号
mov ch , 0 ; 磁头号
mov cl , 2 ; 起始扇区号,从第二个扇区开始读,
; 第一个扇区是引导扇区,第二个才是内核所在
mov al , 17 ; 需读入扇区的数量,此处读了17个扇区,
; 是怕内核较大,读少了读不完
int 13h ; 调用 BIOS 13 号中断开始读扇区,
; 此中断会将数据读到 es:bx 处
jc .ReadFloppy ; 如果出错则重试(ah中是错误号,为0则没错)
mov dl , [bootdrive] ; 停止驱动器
mov edx , 0x3f2
mov al , 0x0c
out dx , al
cli ; 关中断
lgdt [gdt_addr] ; 载入 GDT 的描述符
mov eax , cr0 ; 下面三句设置 cr0 的第 0 位(PE位)为1,
; 表示进入保护模式
or eax , 1
mov cr0 , eax
jmp gdt_code_addr:code_32 ; 跳入32位的代码段中
[BITS 32]
code_32:
mov ax , gdt_data_addr ; 以下三句设置 DS,ES,SS,FS,GS
; 为数据段描述表的位置
mov ds , ax
mov es , ax
mov ss , ax
mov fs , ax
mov gs , ax
mov esp , 0xffff ; 设置堆栈的头指针
jmp gdt_code_addr:0x9000 ; 跳入内核,
; gdt_code_addr是定义的代码段的描述符所在的索引
; 由于我们先前是把内核读到了 0x9000的位置,
; 因此我们现在就转到内核所在去执行,
; 引导程序胜利完成历史使命!
;---------------------------------------------------------------------------
times 510-($-$$) db 0
db 0x55
db 0xAA
下面,我们用我们最熟悉的C语言来编写我们的系统内核。源代码如下:
char* msg = "Welcome to HIT Operation System!Version 0.0001 by xyb " ;
void main()
{
unsigned char* videomem = ( unsigned char* )0xb8000 ; /* 获得显存地址 */
while( *msg != '\0 ' ){
*videomem++ = *msg++ ; /* 设置显示字符的ASCII码 */
*videomem++ = 0x1b ; /* 设置文字属性(背景色,前景色,是否闪烁等)*/
}
for(;;);
}
这个代码太简单了,就不详加解释了,这里只需要说明一下0xb8000是啥地方。让我们首先
来看一下内存是怎样被CPU分配的。
0x0000:0x0000 -> 0x0000:0x03FF = 中断相量表住的地方
0x0000:0x0400 -> 0x0000:0x04FF = BIOS 住的地方 (BIOS 数据区 BDA)
0x0000:0x0500 -> 0x0000:0x7BFF = 你可以使用的自由内存区
0x0000:0x7C00 -> 0x0000:0x7DFF = 系统引导程序的家,
现在你知道引导程序中要加上[ORG 0x7c00]了吧
0x0000:0x7E00 -> 0x9000:0xFFFF = 你可以使用的自由内存区
0xA000:0x0000 -> 0xB000:0xFFFF = VGA 显示器的显示内存所在!!!!!!!!(*)
0xC000:0x0000 -> 0xF000:0xFFFF = BIOS 住的地方 (BIOS 的只读内存区)
。。。。。。。。。
上面列出的就是CPU保留的内存部分的分配情况,注意*行,再看看C语言程序,显然,我们
直接把数据写到显存中了!这样,数据就可能被显示出来。
现在我们的程序已经编制完成,接下来,需要完成的工作是生成我们的启动盘启动我们可
爱电脑,就快熬出头了~~~,累啊!!!
先编译我们的引导程序,然后把它写到引导文件img的头512字节中,这一点在上一篇里已
经详细描述过了,这里就不在描述了,若有不清楚的地方,请找上一篇看看
下面,我们要生成我们的内核,这比较麻烦,因为在windows下却少必要的编译工具与链接
工具,目前的vc,bc都只能生成win32标准的程序,而非二进制文件,弄了好久也没解决这
个问题,有没有哪位大侠知道啊!!!麻烦告诉一声,谢谢!^_^
没办法,MS的东东太烂了,我们只好在Linux下编译它。
在Linux下编写好此C语言程序(也可以在Windows下编写好,然后拷到Linux下用),取名为
xyb.c
然后,建如如下命令:
gcc -c xyb.c
其中 -c 表示只编译不链接
接着再敲
ld -o xyb -Ttext 0x9000 -e main xyb.o
-o 表示输出文件名,-Ttext 0x9000 表示程序基址定为 0x9000 -e main 表示从main()开
始执行
还要敲
objcopy -R .note -R .comment -S -O binary xyb xyb.bin
-R .note -R .comment 表示移掉 .note 与 .comment 段
-S 表示移出所有的标志及重定位信息
-O binary xyb xyb.bin 表示由xyb生成二进制文件xyb.bin
然后,把xyb.bin拷回到Windows中,并用WinHex写到img文件中方才引导程序的后面,OK!
大功告成!
下面是运行的结果:

这里,本文的所有任务与预定目标已经完成。现在的引导程序已经是一个比较完整的引导
程序了,不过内核还不能算是一个内核,如果有时间,我希望能将实验继续下去~~~,还望
大家多多支持,多多指点!^_^
本文所介绍的程序全部源代码及最后生成的映象文件可在下
面地址下载
ftp://202.118.239.46/Incoming/Other/BTC/temp/pyos1.rar

 

在上一篇中我们实现了一个真正意义上的引导程序,此引导程序将计算机从启动状态时的
16位实模式转到了32位的保护模式下,并且将一个用C语言写的真正的内核载入了内存执
行,引导程序的工作已经完成,接下来的工作就是编写一个优良的操作系统的内核,这是
一个非常巨大的工程,我们需要一步一步的来完成,同样,今天我们只完成非常非常小的
一步。在上一篇中,内核只是显示出一个提示语,标志着内核已经启动了,它还不具有同
用户的交互功能,在这一篇中,我们将完成一个可以接受用户键盘输入的内核,这是内核
具有交互功能的第一步——能接受用户输入。

本篇所实现的内核主要采用C++语言书写,中断处理部分用到了很小一段汇编代码,因此
从本篇中你还将知道怎样实现C++与汇编语言的混合编程,本内核开放源码,采用的是C++
类模式的开放结构,你可以很轻松的修改它,让它功能更强,性能理好。

这里想首先介绍一下本内核的程序结构,这样你在阅读源代码的时候,会一目了然。
本内核定义了一个TVideo类,封装了对于VGA显卡的处理,它的声明如下:
class TVideo{
public:
static TPos GetPos() ;
static void SetPos( TPos& pos ) ;
static void SetPos( unsigned short X , unsigned short Y ) ;
static void ClearScreen() ;
static void Print( const char *msg , EColor FrontColor = clWhite ,
EColor BackColor = clBlack ) ;
static void Print( const char msg , EColor FrontColor = clWhite ,
EColor BackColor = clBlack ) ;
EColor BackColor ;
EColor FrontColor ;
protected:
TVideo(){}
};
其中 TPos 也是一个类,(以T开头的,均是类名,以E开头的是枚举类型),其声明如下
class TPos{
public:
unsigned short X ;
unsigned short Y ;
};
其 EColor是一个包含了色彩变量的枚举类型:
enum EColor{ clBlack , clBlue , clGreen , clCyan , clRed , clMagenta ,
clBrown , clLightGray , clDarkGray , clLightBlue , clLightGreen ,
clLightCyan , clLightRed , clLightMagenta , clYellow , clWhite } ;

由于C++语言的封装机制,这使得在程序中要想在屏幕上输出是非常简单的事,下面我们就
来看看主程序对它们调用的例子:
char* msg0 = "Welcome To HIT Operation System!Version 0.0003 by xyb " ;
char* msg1 = "Please input: " ;

EColor color[] = { clLightBlue , clLightGreen , clLightCyan ,
clLightRed , clLightMagenta , clYellow , clWhite ,
clLightBlue } ;
int i = 0 ;
while( msg0[ i ] != '\0 ' ){
TVideo::Print( msg0[ i++ ] , color[ i % 8 ] ) ;
}
TVideo::SetPos( 0 , 2 ) ;
i = 0 ;
while( msg1[ i ] != '\0 ' ){
TVideo::Print( msg1[ i++ ] , clWhite , color[ i % 8 ] ) ;
}
这段代码非常简单,就不详加解释了,它用各种色彩打印提示信息,下面就是它的执行效


下面是接受用户输入后的效果

下面,我们将来看看这都是怎么实现的。

阅读本文,最好有那么一点的汇编基础,另外,最好已经阅读过前两篇,因为很多东东是
同前两篇,特别是第二篇相关的~~~

好了,下面开始转如正题,首先,先介绍一下怎样处理显卡,
在上一篇中,我们已经知道了,通过把数据直接写到显存中就可以在屏幕上显示,这里我
们将更深入的介绍一下

现在的显卡大多是VGA标准兼容显卡,它分字符与图形模式,本文只介绍字符模式。在字符
模式下,它有25行,每行有80列,显存的地址为于0xb8000处,对于需要显示的每一个字符
,用两个字节来描述,第一个字节是要显示的这个字符的ASCII码,第二个字节是要显示的
这个字符的色彩属性,其中高4位用来表示背景色,低4位用来表示前景色,也就是字符本
身的色彩,色彩的对照表如下:
0 Black 黑色
1 Blue 蓝色
2 Green 绿色
3 Cyan 青色
4 Red 红色
5 Magenta 洋红
6 Brown 棕色
7 Light Gray 高亮灰色
8 Dark Gray 暗灰色
9 Light Blue 高亮蓝色
A Light Green 高亮绿色
B Light Cyan 高亮青色
C Light Red 高亮红色
D Light Magenta 高亮洋红
E Yellow 黄色
F White 白色
因此,你可以组合出你想要的字体色彩,怎样组合?请参见源程序相关代码。

我们知道 0xb8000 是显存的起始地址,也就是 0,0 处的字符所在的地址,那么 x,y 处的
字符位置在哪儿呢?因为一行显示80个字符,共有25行,所以我们可以用如下的公式计算
出 x,y 处的字符在内存中的地址:0xb8000 + y*80 + x
因此如果你想在 x,y处显示一个红色的‘A’你可以这样做
char *video = 0xb8000 ;
video += y*80 + x ;
*video = 'A ' ;
video++;
*video = 0x04 ;

下面我们看看怎样处理光标
首先,我们要知道我们有两个端口,端口号分别是0x3d4,0x3d5。这第一个端口用于提供
索引,第二个端口用于提供数据。光标的位置存放在以14,15为索引值的两个端口寄存器中
。每一个端口寄存器只有8位,14号寄存器放存光标的低8位,15号寄存器存放光标的高8位

比如,我们要把光标定位到 x,y 处,首先我们要得到此处的偏移量:offset = y * 8 + x

然后把低8位放到 14号寄存器里,高8位放到15号寄存器里,就像这样:
out 0x3d4 , 14 ;//指定访问14号寄存器
out 0x3d5 , offset > > 8 ;
out ox3d4 , 15 ;
out 0x3d5 , offset ;
(注,这不是最终可执行的汇编代码,只是一个示意代码,实际代码请参考源程序)
要得到光标位置可以读这两个寄存器的值,得到偏移量,然后换算成x,y,详情请参看源程
序。

 

下面,我们将进入主题,将讲述一下怎样处理键盘输入。
处理键盘输入有两种方式,一是通过循环就行,在主程序中不断的查询0x60端口是否有数
据,如果有数据则表示有键盘输入,且此数据就是输入的键的键盘扫描码,将扫描码转换
成相应的ASCII码,然后显示就行。 这种情况非常简单,我们就不详细描述了,你可以改
动本源程序用此种方式实现。这里,我们常用一种新的方式进行,这就是通过中断进行。

要使用中断方式,我们就必须编写自己的中断处理程序,并且要让CPU知道此中断的中断处
理程序在什么地方,这通过IDT(中断描述符表)完成。此表的每一个表项对应一个中断,
每一个表项都指明此中断的中断处理程序在什么地方。因此首要的任务是要构造一个中断
描述符表。

中断描述符表一共可有256项,即256个中断。头三十二项,也就是0~31号中断,已经被CPU
及硬件所占用了,因此我们需要从第三十三项即32号中断开始构造我们自己的中断及中断
服务程序
中断描述符每项占8个字节(64位),所以我们定义如下的一个结构来处理它:
typedef struct{
unsigned long dword0 ;
unsigned long dword1 ;
} segment_desc ;
下面是我们定义中断描述符表的程序:
segment_desc idt[ 256 ] ; /* 中断号 0~255 */
unsigned long idt_desc[ 2 ] ;
unsigned long idt_addr ;
unsigned long keyboard_addr ;
unsigned long idt_offset = 0x8 ; /* IDT 在 GDT 中的位置,此程序中也是代码段在
GDT中的位置 */

// 发送4个ICW
ToPort( 0x20 , 0x11 ) ;
ToPort( 0xA0 , 0x11 ) ;
ToPort( 0x21 , 0x20 ) ;
ToPort( 0xA1 , 0x28 ) ;

ToPort( 0x21 , 0x4 ) ; // 在 Inter 出产的硬件中,PIC之间的联系是
ToPort( 0xA1 , 0x2 ) ; // 把 PIC1的IRQ2 同 PIC2 的IRQ1 联系起来

ToPort( 0x21 , 0x1 ) ;
ToPort( 0xA1 , 0x1 ) ;

// 下面设定中断屏蔽字,只许可键盘中断
ToPort( 0x21 , 0xFD ) ;
ToPort( 0xA1 , 0xFF ) ;

keyboard_addr = ( unsigned long )keyboard_interrupt ; /* 键盘中断处理程序
的位置 */
idt[ 0x21 ].dword0 = ( keyboard_addr & 0xffff ) | ( idt_offset < < 16 ) ;
idt[ 0x21 ].dword1 = ( keyboard_addr & 0xffff0000 ) | 0x8e00 ;

/* 得到整个IDT的位置描述 */
idt_addr = ( unsigned long )idt ;
idt_desc[ 0 ] = 0x800 + ( ( idt_addr & 0xffff ) < < 16 ) ;
idt_desc[ 1 ] = idt_addr > > 16 ;

__asm__( "lidt %0\n " "sti " : "=m "( idt_desc ) ) ; /* 用lidt指令载入 IDT 表,
并用 sti 指令开中断*/
下面我们来解释一下这个程序
ToPort是程序定义的一个函数,具体代码请见源程序,这里只需要知道,它把由第二个参
数指定的数据,发到由第一个参数指定的端口中去。
先来看看这两行
idt[ 0x21 ].dword0 = ( keyboard_addr & 0xffff ) | ( idt_offset < < 16 ) ;
idt[ 0x21 ].dword1 = ( keyboard_addr & 0xffff0000 ) | 0x8e00 ;
一个 IDT 表项有64位长,0~15位是中断处理程序地址的低16位,16~31是中断处理程序所
在的段在GDT中的位置(参见第二篇)。
最高的16位是中断处理程序地址的高16位,而留下的中间的16位是用来表明此是一个中断
门还是一个陷阱门还是一个任务门,及是16位中断还是32位中断等,非常复杂,要想详细
了解请查看Intel CPU 开发人员手册(网上有下的,没找到的可以找我要)。幸运的是,
我们不需要管得太多,只需记住在正常情况下是置为0x8e00就行。

下面我们详细讲一下代码中的“// 发送4个ICW”部份

我们现在已经知道,要建立中断,我们需要填充 IDT( 中断描述符表),它需要指出当发
生中断时,应跳到哪儿去执行。

为了使中断系统起作用,我们需要对PIC(可编程的中断控制器)进行编程,PIC 是可编程
的中断控制器,它可以处理硬件中断请求(IRQ0,IRQ1..等等),如果没有PIC,我们就不得
不按规则去查询哪一个硬件发生了中断,有了PIC,当硬件发生中断时,PIC把中断信号送
到CPU,CPU处理中断。我们实际上有两上PIC,第一个PIC1(端口号0x20~0x21)处理IRQ0~IR
Q7的请求,第二个PIC2(端口号0xA0~0xA1)处理 IRQ8~IRQ15 的请求

CPU只知道逻辑意义上的中断,不区分是物理上的软件中断还是硬件中断,因此我们必须把
CPU不知道的物理中断,映射为CPU知道的逻辑意义上的中断。在实模式下,这项工作由BIO
S来做,在保护模式下需要我们自己做。

因此我们需要初使化PICs,这通过发送一些ICW(初始化命令字)来实现对PICs的控制,它
们必须被精确的依次序发送,因为,它们之间是相互依赖的
1. 发送 ICW1 到 PIC1(20h) 与 PIC2(A0h) 中
2. 发送 ICW2 到 PIC1(21h) 与 PIC2(A1h) 中
3. 发送 ICW3 到 PIC1(21h) 与 PIC2(A1h)中
4. 发送 ICW4 到 PIC1(21h) 与 PIC2(A1h)中

ICW1 用来告诉PIC, 存在ICW4,(当两个PIC串联工作时,这是必须的)
ICW2 用来告诉PIC,把 IRQ0 与 IRQ7 映射到什么地方
(每一个PIC有八个管脚处理中断(IRQ0~IRQ7)
ICW3 用来告诉PIC,它们之间应当用几号IRQ(第几根管脚)进行同信
ICW4 用来告诉PIC,我们工作在保护模式,并且是由软件来处理还是自动处理中断

ICW1的结构
7 6 5 4 3 2 1 0
0 0 0 1 M 0 C I

I : 如果 ICW3 后面还有 ICW4,则置位
C : 如果不置位,表明这两个 PIC 工作在串联状态下
M : 表明 IR0 到 IR7 的线是水平触发,在PC机中,这位应为0(边沿触发)

ICW2 表明了 IRQ0 在中断向量表中的地址,在保护模式下,你应当改变它
7 6 5 4 3 2 1 0
A7 A6 A5 A4 A3 0 0 0

IRQ1 在中断向量表中的地址为 IRQ0的+1,IRQ2~IRQ7以此类推

ICW3 只在 这两个PIC是在串联工作状态下才被发送(ICW1 的C位置0),它的目的是在两
个PIC间建立联系

ICW3 关于 PIC1 的结构
7 6 5 4 3 2 1 0
IR7 IR6 IR5 IR4 IR3 IR2 IR1 IR0

如果 IR0 是置0的,则表明此根线联到一个外围设备
如果 IR0 是置1的,则表明此根线与PIC2联结
其余的与此类似

ICW3 关于 PIC2 的结构
7 6 5 4 3 2 1 0
0 0 0 0 I R Q
最后3位是PIC1的,与PIC2相联结的IRQ号

ICW4 的结构
7 6 5 4 3 2 1 0
0 0 0 0 0 0 EOI 80x86
EOI 表明中断的最后是自动处理还是由软件辅助处理。在PC中此位通常置0,表示软件必须
处理中断的最后扫尾工作
80x86 表明PIC是否工作在80x86的体系结构下

有了上述基础知识,你现在应当可以理解了吧。
下面我们再来看看:中断屏蔽字
PIC 1 处理的中断有
0 系统时钟
1 键盘
2 重定向到IRQ9 (PIC2的IRQ1),如果此位被置1,则屏幕掉所有来自PIC2的中断
3 串口 1 (COM2/4)
4 串口 2 (COM1/3)
5 声卡
6 软驱
7 并行端口

PIC 2 处理的中断有
0 实时时钟
1 来自 IRQ2 (PIC1)
2 保留
3 保留
4 鼠标
5 数学协处理器
6 硬盘
7 保留

当某位置位0表示允许其发出中断请求,置为1表示屏蔽其中断请救
程序中,有这样两行代码:
ToPort( 0x21 , 0xFD ) ;
ToPort( 0xA1 , 0xFF ) ;
其中 0xFD 就是 11111101 ;即只允许 PIC1的第二个中断请求,即键盘中断请求。

完工!本篇任务已经胜利完成~~~ ^_^,所有源代码可在如下地址下载
ftp://202.118.239.46/Incoming/Other/BTC/temp/pyos/pyos3.zip

 

 

下面,我们将进入主题,将讲述一下怎样处理键盘输入。
处理键盘输入有两种方式,一是通过循环就行,在主程序中不断的查询0x60端口是否有数
据,如果有数据则表示有键盘输入,且此数据就是输入的键的键盘扫描码,将扫描码转换
成相应的ASCII码,然后显示就行。 这种情况非常简单,我们就不详细描述了,你可以改
动本源程序用此种方式实现。这里,我们常用一种新的方式进行,这就是通过中断进行。

要使用中断方式,我们就必须编写自己的中断处理程序,并且要让CPU知道此中断的中断处
理程序在什么地方,这通过IDT(中断描述符表)完成。此表的每一个表项对应一个中断,
每一个表项都指明此中断的中断处理程序在什么地方。因此首要的任务是要构造一个中断
描述符表。

中断描述符表一共可有256项,即256个中断。头三十二项,也就是0~31号中断,已经被CPU
及硬件所占用了,因此我们需要从第三十三项即32号中断开始构造我们自己的中断及中断
服务程序
中断描述符每项占8个字节(64位),所以我们定义如下的一个结构来处理它:
typedef struct{
unsigned long dword0 ;
unsigned long dword1 ;
} segment_desc ;
下面是我们定义中断描述符表的程序:
segment_desc idt[ 256 ] ; /* 中断号 0~255 */
unsigned long idt_desc[ 2 ] ;
unsigned long idt_addr ;
unsigned long keyboard_addr ;
unsigned long idt_offset = 0x8 ; /* IDT 在 GDT 中的位置,此程序中也是代码段在
GDT中的位置 */

// 发送4个ICW
ToPort( 0x20 , 0x11 ) ;
ToPort( 0xA0 , 0x11 ) ;
ToPort( 0x21 , 0x20 ) ;
ToPort( 0xA1 , 0x28 ) ;

ToPort( 0x21 , 0x4 ) ; // 在 Inter 出产的硬件中,PIC之间的联系是
ToPort( 0xA1 , 0x2 ) ; // 把 PIC1的IRQ2 同 PIC2 的IRQ1 联系起来

ToPort( 0x21 , 0x1 ) ;
ToPort( 0xA1 , 0x1 ) ;

// 下面设定中断屏蔽字,只许可键盘中断
ToPort( 0x21 , 0xFD ) ;
ToPort( 0xA1 , 0xFF ) ;

keyboard_addr = ( unsigned long )keyboard_interrupt ; /* 键盘中断处理程序
的位置 */
idt[ 0x21 ].dword0 = ( keyboard_addr & 0xffff ) | ( idt_offset < < 16 ) ;
idt[ 0x21 ].dword1 = ( keyboard_addr & 0xffff0000 ) | 0x8e00 ;

/* 得到整个IDT的位置描述 */
idt_addr = ( unsigned long )idt ;
idt_desc[ 0 ] = 0x800 + ( ( idt_addr & 0xffff ) < < 16 ) ;
idt_desc[ 1 ] = idt_addr > > 16 ;

__asm__( "lidt %0\n " "sti " : "=m "( idt_desc ) ) ; /* 用lidt指令载入 IDT 表,
并用 sti 指令开中断*/
下面我们来解释一下这个程序
ToPort是程序定义的一个函数,具体代码请见源程序,这里只需要知道,它把由第二个参
数指定的数据,发到由第一个参数指定的端口中去。
先来看看这两行
idt[ 0x21 ].dword0 = ( keyboard_addr & 0xffff ) | ( idt_offset < < 16 ) ;
idt[ 0x21 ].dword1 = ( keyboard_addr & 0xffff0000 ) | 0x8e00 ;
一个 IDT 表项有64位长,0~15位是中断处理程序地址的低16位,16~31是中断处理程序所
在的段在GDT中的位置(参见第二篇)。
最高的16位是中断处理程序地址的高16位,而留下的中间的16位是用来表明此是一个中断
门还是一个陷阱门还是一个任务门,及是16位中断还是32位中断等,非常复杂,要想详细
了解请查看Intel CPU 开发人员手册(网上有下的,没找到的可以找我要)。幸运的是,
我们不需要管得太多,只需记住在正常情况下是置为0x8e00就行。

下面我们详细讲一下代码中的“// 发送4个ICW”部份

我们现在已经知道,要建立中断,我们需要填充 IDT( 中断描述符表),它需要指出当发
生中断时,应跳到哪儿去执行。

为了使中断系统起作用,我们需要对PIC(可编程的中断控制器)进行编程,PIC 是可编程
的中断控制器,它可以处理硬件中断请求(IRQ0,IRQ1..等等),如果没有PIC,我们就不得
不按规则去查询哪一个硬件发生了中断,有了PIC,当硬件发生中断时,PIC把中断信号送
到CPU,CPU处理中断。我们实际上有两上PIC,第一个PIC1(端口号0x20~0x21)处理IRQ0~IR
Q7的请求,第二个PIC2(端口号0xA0~0xA1)处理 IRQ8~IRQ15 的请求

CPU只知道逻辑意义上的中断,不区分是物理上的软件中断还是硬件中断,因此我们必须把
CPU不知道的物理中断,映射为CPU知道的逻辑意义上的中断。在实模式下,这项工作由BIO
S来做,在保护模式下需要我们自己做。

因此我们需要初使化PICs,这通过发送一些ICW(初始化命令字)来实现对PICs的控制,它
们必须被精确的依次序发送,因为,它们之间是相互依赖的
1. 发送 ICW1 到 PIC1(20h) 与 PIC2(A0h) 中
2. 发送 ICW2 到 PIC1(21h) 与 PIC2(A1h) 中
3. 发送 ICW3 到 PIC1(21h) 与 PIC2(A1h)中
4. 发送 ICW4 到 PIC1(21h) 与 PIC2(A1h)中

ICW1 用来告诉PIC, 存在ICW4,(当两个PIC串联工作时,这是必须的)
ICW2 用来告诉PIC,把 IRQ0 与 IRQ7 映射到什么地方
(每一个PIC有八个管脚处理中断(IRQ0~IRQ7)
ICW3 用来告诉PIC,它们之间应当用几号IRQ(第几根管脚)进行同信
ICW4 用来告诉PIC,我们工作在保护模式,并且是由软件来处理还是自动处理中断

ICW1的结构
7 6 5 4 3 2 1 0
0 0 0 1 M 0 C I

I : 如果 ICW3 后面还有 ICW4,则置位
C : 如果不置位,表明这两个 PIC 工作在串联状态下
M : 表明 IR0 到 IR7 的线是水平触发,在PC机中,这位应为0(边沿触发)

ICW2 表明了 IRQ0 在中断向量表中的地址,在保护模式下,你应当改变它
7 6 5 4 3 2 1 0
A7 A6 A5 A4 A3 0 0 0

IRQ1 在中断向量表中的地址为 IRQ0的+1,IRQ2~IRQ7以此类推

ICW3 只在 这两个PIC是在串联工作状态下才被发送(ICW1 的C位置0),它的目的是在两
个PIC间建立联系

ICW3 关于 PIC1 的结构
7 6 5 4 3 2 1 0
IR7 IR6 IR5 IR4 IR3 IR2 IR1 IR0

如果 IR0 是置0的,则表明此根线联到一个外围设备
如果 IR0 是置1的,则表明此根线与PIC2联结
其余的与此类似

ICW3 关于 PIC2 的结构
7 6 5 4 3 2 1 0
0 0 0 0 I R Q
最后3位是PIC1的,与PIC2相联结的IRQ号

ICW4 的结构
7 6 5 4 3 2 1 0
0 0 0 0 0 0 EOI 80x86
EOI 表明中断的最后是自动处理还是由软件辅助处理。在PC中此位通常置0,表示软件必须
处理中断的最后扫尾工作
80x86 表明PIC是否工作在80x86的体系结构下

有了上述基础知识,你现在应当可以理解了吧。
下面我们再来看看:中断屏蔽字
PIC 1 处理的中断有
0 系统时钟
1 键盘
2 重定向到IRQ9 (PIC2的IRQ1),如果此位被置1,则屏幕掉所有来自PIC2的中断
3 串口 1 (COM2/4)
4 串口 2 (COM1/3)
5 声卡
6 软驱
7 并行端口

PIC 2 处理的中断有
0 实时时钟
1 来自 IRQ2 (PIC1)
2 保留
3 保留
4 鼠标
5 数学协处理器
6 硬盘
7 保留

当某位置位0表示允许其发出中断请求,置为1表示屏蔽其中断请救
程序中,有这样两行代码:
ToPort( 0x21 , 0xFD ) ;
ToPort( 0xA1 , 0xFF ) ;
其中 0xFD 就是 11111101 ;即只允许 PIC1的第二个中断请求,即键盘中断请求。

完工!本篇任务已经胜利完成~~~ ^_^,所有源代码可在如下地址下载
ftp://202.118.239.46/Incoming/Other/BTC/temp/pyos/pyos3.zip

 

 

以前的下载地址是校内的,现在放在公网上了,非常希望大家多多指教

实验1的源程序
http://software.hit.edu.cn/borlandclub/pyos/pyos1.rar
实验2的源程序
http://software.hit.edu.cn/borlandclub/pyos/pyos2.rar
实验3的源程序
http://software.hit.edu.cn/borlandclub/pyos/pyos3.rar

你可能感兴趣的:(程序点滴,keyboard,工作,汇编,磁盘,任务,语言)