When Do We Write Our Chinese OS? (3)

类别:编程语言 点击:0 评论:0 推荐:

在上一篇中我们实现了一个真正意义上的引导程序,此引导程序将计算机从启动状态时的
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
BTW:
在本实验进行的过程中,在BBS上得到了很多老师同学的鼓励,正是由于这种支持力量的
存在,使我获得了将本实验进行下去的力量,在此深表感谢!同时对于此中不计其数的错
误,非常希望各位老师、同学、大牛小牛:P~~,批评指教!
仍然留个mail:
[email protected]  

原文:http://purec.binghua.com/Article/ShowArticle.asp?ArticleID=5

本文地址:http://com.8s8s.com/it/it28782.htm