Hacker 的个人资料斌的共享空间照片日志列表更多 工具 帮助

Hacker

职业
地点
兴趣
study in Xidian University
列表
尚未添加列表。

自定义 HTML

没有添加内容。
尚未添加列表。
尚未添加列表。

今日有感

不要轻易麻烦别人
除非是不得已的事情
自己的事情自己做
雷厉风行
 

时间过得真快

时间过得真快啊
转眼就大三了
明年的这个时候就找工作了
大学就这样过去了
初中时代
高中时代
真的就成了远处大山上掠过的云朵
那样 轻轻的 越飘越远
那种童真奇趣 那种欢声笑语
就着流水
一点一滴
敲打着我的心扉
明天... ...
明天应该更美好……
ola.photo.qq

探究------操作系统的引导过程

前言
本篇文章并不旨在完整的讨论一个多引导系统程序怎样去引导不同的操作系统,而只
打算从编写操作系统的角度出发,谈谈计算机怎样从加电开始,从无到有,将操作系统运行
起来,在其中将尽量详尽的描述从实模式到保护模式的过渡,目的只在于能将所学与广大爱
好者共享,为希望开发操作系统的朋友留下一点资料,也为自己留下一点心得。
本篇文章将以开发中的pyos 系统引导程序为例,pyos 是一个正在开发中的实验型操作
系统,它并不打算以目前任何一种运行中的操作系统为模式,而只想通过自己编写一个从头
到尾的操作系统来学习知识,积累技术,如果你有兴趣,非常欢迎你的加入!
本篇纯属学习过程中的一点心得体会,如果你发现其中有错误或不当之处,非常希望
你来信指教。
一、计算机从加电开始都做了什么?
当机算机的电源键被按下时,同这个键相联的电信号线就会送出一个电信号给主板,
主板将此电信号传给供电系统,供电系统开始工作,为整个系统供电,并送出一个电信号给
BIOS,通知BIOS 供电系统已经准备完毕。随后BIOS 启动一个程序,进行主机自检,主机
自检的主要工作是确保系统的每一个部分都得到了电源支持,内存储器、主板上的其它芯片、
键盘、鼠标、磁盘控制器及一些I/O 端口正常可用,此后,自检程序将控制权还给BIOS。
接下来BIOS 读取BIOS 中的相关设置,得到引导驱动器的顺序,然后依次检查,直到找到
可以用来引导的驱动器(或说可以用来引导的磁盘,包括软盘、硬盘、光盘等),然后调用
这个驱动器上磁盘的引导扇区进行引导。BIOS 是怎么知道或说分辨哪一个磁盘可以用来引
导的呢?
二、认识引导程序
BIOS 将磁盘的第一个扇区(磁盘最开始的512 字节)载入内存,放在0x0000:0x7c00
处(见图三),如果这个扇区的最后两个字节是“55 AA”,那么这就是一个引导扇区,这个
磁盘也就是一块可引导盘。通常这个大小为512B 的程序就称为引导程序(boot)。如果最后
两个字节不是“55 AA”,那么BIOS 就检查下一个磁盘驱动器。
通过上面的表述我们可以总结出如下三点引导程序所具有的特点:
1. 它的大小是512B,不能多一字节也不能少一字节,因为BIOS 只读512B 到内
存中去。
2. 它的结尾两字节必须是“55 AA”,这是引导扇区的标志。
3. 它总是放在磁盘的第一个扇区上(0 磁头,0 磁道,1 扇区),因为BIOS 只读

第一个扇区。

(图一)
因此,在我们编写引导程序的时候,我们也必须注意上面的三点原则,符合上面三点
原则的程序都可以看作是引导程序,至少BIOS 是这样认为的,虽然它也许可能是你随意写
的一段并没有什么实际意义的代码。
因为BIOS 一次只读一个扇区也即512 字节的数据到内存中,这显然是不够的,现在操
作系统都比较庞大,因此我们必须在引导扇区里,将存在磁盘上的操作系统的核心部分读进
内存,然后再跳转到操作系统的核心部分去执行。
三、通过BIOS 读磁盘扇区
从上面的描述我们可以知道,引导程序需要将存在于磁盘上的操作系统读入内存,因
此这里我们不得不再讲一讲,怎样不通过操作系统(因为现在还没有操作系统)去读取磁盘
上的内容。一般说来这有两种方法可以实现,一种是直接读写磁盘的I/O 端口,一种是通过
BIOS 中断实现。前一种方法是最低层的方法(后一种方法也是在它的基础上实现的),具有
极高的灵活性,可以将磁盘上的内容读到内存中的任意地方,但编程复杂。第二种方法是前
一种方法稍微高层一点的实现,牺牲了一点灵活性,比如,它不能把磁盘上的内容读到
0x0000:0x0000 ~ 0x0000:0x03FF 处。为什么不能读到此处呢?这里我们将不得不描述一下
CPU 在加电后的中断处理机制。
3.1 BIOS 的中断处理
中断是什么?相信学过计算机的人都不会陌生,如果你对中断一点都不了解建议你翻
看一下《计算机组成原理》(高等教育出版社唐朔飞),上面有非常详尽的描述,而一般的
汇编教材也多有谈及,因此这里只打算讲讲BIOS 对中断的处理。





中断地址形成部件0x03ff jmp ……



0x0000 jmp ……
0x0001 jmp ……
0x0002 jmp ……
0x0003 jmp ……
(图二)
由上图(图二)我们可以清楚的看到,当中断信号产生时,中断信号通过“中断地址
形成部件”产生一个中断向量地址,此向量地址其实就是指向一个实际内存地址的指针,而
这个实际内存地址中往往安排一条跳转指令(jmp)跳转到实际处理此中断的中断服务程序
中去执行。这一块专门用于处理中断跳转的内存就被称为中断向量表。在内存中,这块中断
向量表被放在什么地方呢?而实际的中断处理程序又在什么地方呢?
3.2 系统的内存安排(1M)
要回答上面的两个问题,我们需要看看系统中内存是怎么安排的。在CPU 被加电的时
候,最初的1M 的内存,是由BIOS 为我们安排好了的,每一字节都有特殊的用处。

0x00000~0x003FF: 中断向量表
0x00400~0x004FF: BIOS 数据区
0x00500~0x07BFF: 自由内存区
0x07C00~0x07DFF: 引导程序加载区
0x07E00~0x9FFFF: 自由内存区
0xA0000~0xBFFFF: 显示内存区
0xC0000~0xFFFFF: BIOS 中断处理程序区
(图三)
由上图我们现在可以很方便的问答上面提出的两个问题。由于0x00000~0x003FF 是中
断向量表所在,因此不能将磁盘中的操作系统读到此处,因为这样会覆盖中断向量表,就无
法再通过BIOS 中断读取磁盘内容了。你也许会说:我是先调用中断,再读的啊。但事实上
BIOS 在读的过程中自己会多次调用其它中断辅助完成。
3.3 利用BIOS 13 号中断读取磁盘扇区
有了前面的描述作为基础,下面我们可以正式描述怎样通过BIOS 中断读取磁盘扇区
了。要读取磁盘扇区,我们需要使用BIOS 的13 号中断,13 号中断会将几个寄存器的值作
为其参数,因此,我们在调用13 号中断的过程中需要首先设置寄存器。那么当怎样设置寄
存器呢?会用到哪些寄存器呢?请往下看:
AH 寄存器:存放功能号,为2 的时候,表示使用读磁盘功能
DL 寄存器:存驱动器号,表示欲读哪一个驱动器
CH 寄存器:存磁头号,表示欲读哪一个磁头
CL 寄存器:存扇区号,表示欲读的起始扇区
AL 寄存器:存计数值,表示欲读入的扇区数量
在设置了这几个寄存器后,我们就可以使用int 13 这条指令调用BIOS 13 号中断读取
指定的磁盘扇区,它将磁盘扇区读到ES:BX 处,因此,在调用它之前,我们实际上还需要

设置ES 与BX 寄存器,以指出数据在内存中存放的位置。
四、保护模式下,段模式内存地址的访问
写程序离不开对内存的访问,然而在保护模式下内存的访问与在实模式下内存的访问
完全不同,这里我们将详细描述一下保护模式下内存的访问方法。当然,这里并不打算完整
的介绍保护模式下所有的内存访问方法与机制,只介绍从实模式转到保护模式下所需要进行
的转换,完整的内存访问请你参见《Intel 用户手册》。当然,随着pyos 的实验进行,我也会
在后面的实验报告与心得体会中渐渐描述。
4.1 实模式下的内存访问
计算机在加电时,处于“实模式”,在计算机中有一个CR0 寄存器,又称为0 号控制寄
存器,在这个寄存器中,最低位也即第0 位,被称为PM(Protected Modle :保护模式)位,
当它被清零的时候表示CPU 在“实模式”下工作,当它被置位的时候表示CPU 在“保护模
式”下工作。在计算机加电的时候,它是被清零的,所在这个时候的计算机,处于“实模式”。
“实模式”下的内存访问通过段寄存器与偏移量构成,比如前面描述中常常出现的
0x:0000:0x0001 就是一个实模式下的内存地址。分号前面的值表示段寄存器中的值,分号后
面的值表示偏移量,实际物理地址的形成如下图所示:
15 0 15 0
左移4 位
+
段寄存器偏移量
19 0

20 位的物理地址
(图四)
然而在保护模式下,内存地址却不是如上图所示的方法形成的。那么它又是怎样形成
的呢?
4.2 保护模式下的内存地址形成
保护模式下内存地址就复杂多了,我们首先要分清三个概念:逻辑地址、线性地址与
物理地址。物理地址很好理解,逻辑地址也好理解,就是程序所使用的地址。那么什么是线
性地址呢?
其实如果不使用分页机制的话,线性地址就是物理地址,它与物理地址是一一对应的,
线性地址0,也就是物理地址0。但我们知道,32 位的CPU 拥有32 根地址线,也就是可以
访问:
32
2 = 4GB
的内存空间,这实在是一个太大的空间了!现在很少有机器的物理内存能有这么大。那怎么

在有限的物理空间中使用4GB 的空间呢?人们把物理内存分成许多页,同样也把整个4GB
的线性地址空间分成大小相同的许多页。在线性地址空间中,当某些页被使用的时候,某些
页可能没有被使用,操作系统可以让CPU 将没有被使用的页调出物理内存(存放在磁盘的
某个地方,以备需要的时候再次调入),而把需要使用的页调入,这样,虽然物理内存空间
有限,但也几乎可以使用所有的线性地址空间了。这就称为从线性地址到物理地址的映射,
这是一个多对一的映射,也就是说多个线性空间中的页对应一个物理空间中的页,希望下面
一幅图能有助于你理解这样的分页机制。
页1
页2
页3
页4
页5
页1
页2
页3
页4
页6
页7
页8
(图五)
上面是一种最简单的映射方式,术语称作“直接相连”映射,它大约只能用来说明问
题,而在一个实际的操作系统中通常是“全相联相连”映射,也就是说线性地址中的页可以
是映射到物理地址中的任何一个页中,只要那块物理地址空间现在是空闲的。不过,通过上
图也能说明问题,当线性地址中的页5 需要被访问时,CPU 通过地址映射机制将其转换到
物理地址,发现其对应物理地址中的页1。于是CPU 会产生一个所谓的缺页中断来通知操
作系统进行处理,操作系统相应这个中断,并在中断服务程序中将物理地址页1 中的内容放
到磁盘上的一个地方(虚拟内存),然后将线性地址中的页5 载入物理内存页1 中。这里就
当可以比较明显的区别什么是线性地址,什么是物理地址了。
然而,当不使用分页机制的时候,线性地址就会被CPU 当做物理地址来使用,线性地
址会被直接放在CPU 的地址信号线上。不过,在编写应用程序的时候,我们通常使用的却
是另一种地址——逻辑地址,从逻辑地址到线性地址也存在着与上述机制类似的一种映射机
制,不过这个机制常常称为“段模式”,它是由操作系统与CPU 硬件共同完成的。操作系统
的任务就是分配映射表,而CPU 硬件的任务就是按着映射表进行映射。而这样的映射表在
操作系统编写中又称之为“描述符表”,有两种重要的描述符表,一种是“全局描述符表
(GDT)”,另一种是“局部描述符表(LDT)”,这两种表的用途不同,但它们的用法却是近
似的,下面我们就来描述一下全局描述符表。
说到表,学过数据结构的人都知道,其实它就是一种数据结构,全局描述符表也是一
种数据结构,当这种结构放在一块连续的内存之中就称之为表了。表由表项组成,全局描述
符表由它的表项“全局描述符”组成,其实单纯的术语就叫“描述符”,只因为它放在全局
描述符表中就成了全局描述符了。这个描述符由8 个字节组成,下面我们就来看看它的结构:

段基址的0~24 位段限的 0~15 位
7 6 5 4 3 2 1 0
TYPE:4 位
S 标志:1 位
DPL 标志:2 位
段限的16~19 位
AVL 标志:1 位
0:1 位
D/B 标志:1 位
G 标志:1 位
基址的24~31 位
P 标志:1 位
(图六)
TYPE: 表明此段的类型,4 位中的最高位被清0 的时候表是它是数据段,相应的余下
的三位,从左到右依次为E、W、A,即数据段的TYPE 为:0EWA 。其中E 表示向下增长
位,置1 时表示向下增长(这主要是在大小需要动态改变的堆栈段中使用,如果是向下增长
的段,动态的改变段的大小限制,会让堆栈空间加到堆栈的底部。如果堆栈的大小不需要改
变,那么这个段既可以是向下增长的段,也可以是非向下增长的段);W 表示可写位,置1
表示可写;A 表示被访问位(如果CPU 访问了它,此位将会被置1)。
4 位中的最高位被置1 时表示它是代码段,相应的余下的三位,从左到右依次为C、R、
A,即代码段的TYPE 为:1CRA。其中C 是表明此代码段是否是一致代码段,如果C 被置
1,表明此是一致代码段。一致代码段主要是用于特权级访问控制,这在以后的实验报告中
会详细论述;R 表明此段是否可读,置1 表示可读;A 表示被访问位,这与前述一样。
S:为1 时表示这是一个代码段或数据段描述符,为0 时表示这是一个系统段描述符。
系统段描述符又称为特殊段描述符,包括:局部描述符表(LDT)描述符,任务状态段(TSS)
描述符,调用门描述符,中断门描述符,陷阱门及任务门描述符等。
DPL:表示特权级,从00~11 ,共0,1,2,3 四个特权级
P:为0 是表示此描述符无效,不能被使用
AVL:留给程序员随便用的

D/B:为0 的时候表示它是一个16 位的段,为1 时表示它是一个32 位的段
G:为0 时,表示段限的单位是1 字节,为1 时表示段限的单位是4KB ,并且段偏移量
的最低12 位将不被检测是否在段限之中。(这一点现在可能不好理解,但我下面马上会解
释)。
这里面有两个部份比较有意思,一个是“基址”,一个是“段限”。基址应当比较好理
解,它给出的是一个段在线性内存中的起始地址,对于“段限”,顾名思义,就是段大小的
限制。不过它有点特别,对于一个段的最大可访问的地址CPU 是通过下面的公式计算得到
的:
段基址 + 段限值 * 段限单位 = 此段最大可访问地址
如果一个偏移地址大于了此段最大可访问地址的话,CPU 就将产生一个错误中断,这
样一来就可以防止一个程序非法访问另一个程序的内存空间,这对内存起到了保护作用,“保
护模式”由此得名。
所以,如果段限是0,那么此段最大可访问地址就是段的基址,因此,当段限单位为一
字节时,此段的段大小就是1 字节;当段限单位为4KB 时,因为CPU 将不检测偏移量的最
低12 位,而这12 位最大可能为0xFFF ,因此,这时此段的可访问范围就为4KB ,所以:
(段限值 + 1)* 段限单位 = 此段大小
现在我们可以正式开始描述在保护模式下段模式是怎样访问内存的了。这里之所以要
强调“段模式”是因为在保护模式下还有一种前面叙述过的内存访问模式——页模式,它负
责将线性地址再按某种映射转换为物理地址。“页模式”也是基于段模式的,在不使用它的
情况下,线性地址会被直接放到地址线上当做物理地址使用。“段模式”是不可避免的,所
谓的“纯页模式”只是将整个线性地址当作一个整段,没有什么方法可以真正绕过“段模式”,
因为这是由CPU 内存访问机制所规定的。本篇只描述段模式。
我们已经知道从程序使用的逻辑地址到线性地址的映射是通过“描述符”来完成的,
而“描述符”又是放在描述符表中的,那么,一个描述表中有许多描述符,到底选用哪一个
描述符呢?这就由一个索引来决定,这个索引将指出是表中的第几个描述符,这个索引有一
个专门的术语来描述,常常称它为“段选择子”。“段选择子”由2 个字节共16 位组成,下
面,我们就来看看它提供了哪些信息:
索引值TI RPL
15 3 2 10
(图七)
其中:
RPL:指示出特权级,00~11 ,共0、1、2、3 四个特权级,与前述一样。
TI:为0 时表明这是个用于全局描述符表的选择子,为1 时表明用于局部描述符表。
索引值:用来指示表中第几个描述符。索引值共有13 位,因此,每张描述符表共
可有8K 个表项,而一个表项如前所述,占8 个字节,因此一张描述符表最大可达 64K。
不知道大家是否注意到这样一个事实,如果将“段选择子”的最后3 位置0,这整个段
选择子其实就是一个描述符在描述符表中的偏移量!这里我们可以发现Intel 的工程师在设
计的时候真的是非常精巧,如此的安排,可以使选取一个描述符的速度极大加快,因为将一
个段选择子最后3 位清零后与描述符表的基址相加,就立即可以得到一个描述符的物理地

址,通过这个地址就可以直接得到一个描述符。那么这个描述符表的基址又是放在哪儿的
呢?
所为描述符表的基址也就是此描述符表在内存中的起始地址,也即表中第一个描述符
所在的内存地址,系统中用两个特殊的寄存器来存放,一个用于存放全局描述符表的基址,
称之为“全局描述符表寄存器(GDTR)”,另一个用来存放局部描述符表的基址,称之为“局
部描述符表寄存器(LDTR)”,它们的结构如下图所示:
基址表限
47 16 15 0
(图八)
其中表限也即表的大小限制,它的使用与前面所描述的段限是类似的,因此,这里就不
在描述了。
在保护模式下,以前实模式下的段寄存器还是有用的,不过它不再用来存放段的基址,
而是用来存放“段选择子”,它的名字也变成了“段选择子寄存器”,在访问内存的时候,我
们需要给出的是“段选择子”,而不是段基址了。
比如,我现在想使用全局描述符表中第二个表项,即其中的第二个“段描述符”,这个
“段选择子”就需按如下的方式构成:
RPL:00,因为我们现在是在写操作系统,工作在0 特权级
TI:0,我们使用全局描述符
索引值:1,我们使用第二个全局描述符,第一个全局描述符编号为0,第二个为1
因此,我们的“段选择子”为:0000 0000 0000 10000 ,也即 0x0008 ,因此,对于
0x0008:0x0000 这样一个逻辑地址,在保护模式下就应看成是使用全局描述符段中第二个描
述符所描述的段,并且偏移量为0 的内存地址。
这个逻辑地址的线性地址是怎样形成的呢?请看如下的图示:
段选择子寄存器
0x0008
偏移量
0x0000



全局
描述
符表
基址
* 8
+
描述
符地

全局描述符表
一个描符
段基址
+
32 位线性地址
全局描符寄存器

(图九)
相信,从上图中你可以清楚的看出一个逻辑地址是怎样转换为一个32 位的线性地址的。
五、pyos 引导程序编写
pyos 是一个正在编写中的操作系统,是一个实验中的项目,关于编写的目的与动机我
已在前言中谈论过了,这里,仅就此篇所讲述的内容,谈谈pyos 引导程序的编写,在编写
期间参考了linux 0.11 内核引导程序的编写,不过pyos 并不是基于linux 的,就它们的引导
程序之间也有许多不一样的地方。下面我们先来看看pyos 的整个引导区的内存安排:
0x0000
0x7C00
0x7CFF
0x90000
0x900FF
0x90100
0x904FF
0x90500
Boot 程序区
Setup 程序区
数据存放区
全局描述符表GDT 区
System 程序暂存区
System 程序区
(图十)
上面一幅图就是pyos 的内存安排图,也是引导程序流程图,pyos 是两级引导系统,首
先是boot 被BIOS 读入,随后boot 读入setup,setup 读入system 程序到暂存区,然后把system
程序搬到内存顶部,并建立指向system 程序所在段的段描述符及建立GDT,然后切换CPU
到保护模式,最后跳转到system 程序中执行,至此pyos 系统引导完毕,system 程序将是pyos
真正的系统内核。图中的数据存放区用来存发在boot 、setup、pyos 三者程序间需要传递的
参数。
之所以做成两级引导主要是考虑到以后扩展时的方便,各程序间都差不多是独立的,
以后可以重写boot 或者setup 以提供更多可选择的引导方式。System 程序暂存区是因为如

前所述,不能直接将数据读到中断向量表中覆盖原中断向量表,当数据读完之后,不再调用
中断了,才将程序搬到内存顶部覆盖原中断向量表,对于保护模式下的中断向量表,将由
system 程序负责建立,交给system 使用的是一块完整而干净的内存。
对于pyos 进程内存安排,准备参照Linux 0.11 进行,内存安排如下:

(图十一,来源《Linux 0.11 内核代码完全注释》)
一个进程享有64M 空间,4GB / 64M = 64 ,也即系统最大进程数为64。因此一个段的
段限为:64MB ,每个进程占用全局描术符表中两个描述符,一个为数据段描述符,一个为
代码段描述符,段限均为64MB 。
六、pyos 引导程序源代码
下面将提供pyos 引导程序的全部源代码,因为system 还未完全完成,因此这里只是让
它简单的打印一个字符以示引导工作完成,代码中已有较为详尽的注释,如果仍有不太清楚
的地方,可以去http://purec.binghua.com (纯C 论坛)操作系统实验专区,查看pyos 以前的
实验报告,上面有非常详尽的注释及相关原理说明,并详细描述了怎样编译及实验。
;文件名: boot.asm
;作 者; 谢煜波
;Emailv: xieyubo@126.com
;
;内存分配如下
;内存起始地址为 0x90000
;最大结束地址为 0x9ffff
;最大共 64KB
;所有启动代码在一个段内,方便调用
;启动代码共分两部分,一是boot,一是setup,这点照搬linux 0.11的设计
;但与其不同的是,boot不会将自己搬到0x90000处,而直接跳到 0x90100处运行
;0x90000~0x900ff (256B) 系统保留来存放一些从BIOS中取出的关键数据
;0x90100~0x904ff (1KB):此处开始存放setup,setup大小为1KB
[BITS 16] ;编译成16位的指令
[ORG 0x7C00]
;-------------------------------------------------------------------------------------------

jmp Main
;-------------------------------------------------------------------------------------------
;数据定义
MSG db "Loading pyos ..." ;输出信息
db 13 , 10 , 0 ;13 表示回车,10 表示换行,
;0 表示字符串结束
BOOTSEG equ 0x0000 ;boot 所在的段基址
SETUPSEG equ 0x9000 ;setup 所在的段基址
SETUPOFFSET equ 0x0100 ;setup 所在的偏移量
SETUPSIZE equ 1024 ;setup 的大小,必须是512 的倍数
BOOTDRIVER db 0 ;保存启动的驱动器号
;-------------------------------------------------------------------------------------------
ShowMessage:
;以下程序行为显示输出信息
mov ah , 0x0e ;设置显示模式
mov bh , 0x00 ;设置页码
mov bl , 0x07 ;设置字体属性
.nextchar:
lodsb
or al , al
jz .return
int 0x10
jmp .nextchar
.return:
ret
;-------------------------------------------------------------------------------------------
Main:
mov [BOOTDRIVER] , dl ;得到启动的驱动器号
;以下程序设置数据段
mov ax , BOOTSEG
mov ds , ax
mov si , MSG
call ShowMessage ;显示信息
 ;读入setup
;从磁盘的第二个扇区读到0x90100处
.readfloopy:
mov ax , SETUPSEG
mov es , ax
mov bx , SETUPOFFSET

 mov ah , 2
mov dl , [BOOTDRIVER]
mov ch , 0
mov cl , 2
mov al , SETUPSIZE / 512 ;读入扇区数( 2个共1KB )
int 0x13
jc .readfloopy
 ;把启动驱动器号保存在0x90000处
mov al , [BOOTDRIVER]
mov [0] , al
;跳转
jmp SETUPSEG : SETUPOFFSET
;---------------------------------------------------------------------------
times 510-($-$$) db 0
db 0x55
db 0xAA
;文件名: setup.asm
;作 者; 谢煜波
;Emailv: xieyubo@126.com
;此setup程序完成boot 未完成的启动工作,
;包括从BIOS 中读出系统信息存放在指定位置
;初始化GDT,LDT表,完成从保护模式到实模式的转换
;实模式的代码也由此程序读入
[BITS 16]
[ORG 0x0100]
;-------------------------------------------------------------------------------------
jmp Main
;-------------------------------------------------------------------------------------
SETUPSEG equ 0x9000
SETUPOFFSET equ 0x0100
SETUPSIZE equ 1024 ;setup 的大小1KB,必须是 512 的倍数
SYSTEMSEG equ 0x0000
SYSTEMOFFSET equ 0x0000
SYSTEMSIZE equ 1024 ;SYSTEM 的大小1KB, 此值必须是 512 的倍

;实际值可以不符
;下面定义临时GDT表的描述符
;总共定义三个段,一个空段由intel保留,一个代码段,一个数据段

gdt_addr:
dw 0x7fff ;GDT 表的大小
dw gdt ;GDT 表的位置
dw 0x0009
gdt:
gdt_null:
dw 0x0000
dw 0x0000
dw 0x0000
dw 0x0000
gdt_system_code:
dw 0x3fff ;段限(0x3fff+1)*4KB=64KB
dw 0x0000
dw 0x9a00
dw 0x00c0
gdt_system_data:
dw 0x3fff
dw 0x0000
dw 0x9200
dw 0x00c0
;-------------------------------------------------------------------------------------
;等待键盘控制器空闲的子程序
Empty_8042:
in al , 0x64
test al , 0x2
jnz Empty_8042
ret
;-------------------------------------------------------------------------------------
Main:
 ;初始化寄存器,因为Bios 中断及call会用到堆栈或ss 寄存器
 ;在CPU启动或复位时是由BIOS 初始化的,而现在进行了段转移,需要我们重新设置
mov ax , SETUPSEG
mov ds , ax
mov es , ax
mov ss , ax
mov sp , 0xffff
 ;-------------------------------------------------------------------------
 ;从BIOS 中到底应读出哪些有用信息,现在还不确定,因此暂时跳过此功能块
;-----------------------------------------------
 ;0x90000 (1B): 保存启动驱动器号,由boot 程序存入
 ;--------------------------------------------------------------------------
 ;下面读入system 到setup 程序的后面

 ;因为0x00000现在是放BIOS 中断的地方,因此还不能直接将system 读到0x00000 处,
;否则将无法调用BIOS 中断读入磁盘
.readfloopy:
mov ax , SETUPSEG
mov es , ax
mov bx , SETUPOFFSET + SETUPSIZE
mov ah , 2
mov dl , [0]
mov ch , 0
mov cl , 1 + 1 + SETUPSIZE / 512 ;system 所在的启始扇区
;第一个1 是指从1 开始记数
;第二个1是boot 所占扇区数
mov al , SYSTEMSIZE / 512 ;读入扇区数( 2个扇区共1KB )
int 0x13
jc .readfloopy
;下面将读入的system 搬移到0x00000 位置
cld
mov si , SETUPOFFSET + SETUPSIZE
mov ax , SYSTEMSEG
mov es , ax
mov di , SYSTEMOFFSET
mov cx , SYSTEMSIZE / 4
rep movsd
;下面开始为进入保护模式而进行初始化工作
cli ;关中断
lgdt [gdt_addr] ;载入gdt 的描述符
;下面打开A20 地址线
call Empty_8042
mov al , 0xd1
out 0x64 , al
call Empty_8042
mov al , 0xdf
out 0x60 , al
call Empty_8042
;下面设置进入32 位保护模式运行
mov eax , cr0
or eax , 1
mov cr0 , eax
jmp dword 0x8:0x0

;-------------------------------------------------------------------------------------
times 1024-($-$$) db 0
;文件名: kernel.asm
;作 者; 谢煜波
;Emailv: xieyubo@126.com
[BITS 32]
[ORG 0x0]
;------------------------------------------------------------------------------------------
jmp Main
;------------------------------------------------------------------------------------------
Main:
;设置寄存器
mov ax , 0x10
mov ds , ax
mov cl , '1'
mov [0xb8000] , cl
mov cl , 0x04
mov [0xb8001] , cl
jmp $
以上程序中有一个地方本篇及以前的实验报告中也未提到,这就是A20 地址线的问题,
对于有关A20 地址线的问题,在《Linux 0.11 内核源代码完全注释》中有非常详细的描述,
作者还列举了其它几种打开A20 地址线的方法,并分析了可能存在的问题。这是一本非常
好的书,推荐大家阅读。纯C 论坛上(http://purec.binghua.com )可以下载本书的电子版(PDF
格式),也可以在上面找到另外一些相关资源。
下面就是运行时的截图,现在它只能引导,什么也干不了,希望下次它能多干一点~~


参考资料
1. 《IA-32 Intel. Architecture Software Developer’s Manual Volume 3:System Programming
Guide 》(Intel 2001)
2. 《Linux 内核 0.11 完全注释》(赵炯,2003)
《计算机组成原理》(唐朔飞,高等教育出版社)


刚来到msn空间

感觉不错哦

[转贴]什么是中断,为什么要用中断?

 


中断解析
一、中断是什么

中断的汉语解释是半中间发生阻隔、停顿或故障而断开。那么,在计算机系统中,我们为什么需要“阻隔、停顿和断开”呢?

举个日常生活中的例子,比如说我正在厨房用煤气烧一壶水,这样就只能守在厨房里,苦苦等着水开——如果水溢出来浇灭了煤气,有可能就要发生一场灾难了。等啊等啊,外边突然传来了惊奇的叫声“怎么不关水龙头?”于是我惭愧的发现,刚才接水之后只顾着抱怨这份无聊的差事,居然忘了这事,于是慌慌张张的冲向水管,三下两下关了龙头,声音又传到耳边,“怎么干什么都是这么马虎?”。伸伸舌头,这件小事就这么过去了,我落寞的眼神又落在了水壶上。

门外忽然又传来了铿锵有力的歌声,我最喜欢的古装剧要开演了,真想夺门而出,然而,听着水壶发出“咕嘟咕嘟”的声音,我清楚:除非等到水开,否则没有我享受人生的时候。



这个场景跟中断有什么关系呢?



如果说我专心致志等待水开是一个过程的话,那么叫声、电视里传出的音乐不都让这个过程“半中间发生阻隔、停顿或故障而断开”了吗?这不就是活生生的“中断”吗?

在这个场景中,我是唯一具有处理能力的主体,不管是烧水、关水龙头还是看电视,同一个时间点上我只能干一件事情。但是,在我专心致志干一件事情时,总有许多或紧迫或不紧迫的事情突然出现在面前,都需要去关注,有些还需要我停下手头的工作马上去处理。只有在处理完之后,方能回头完成先前的任务,“把一壶水彻底烧开!”

中断机制不仅赋予了我处理意外情况的能力,如果我能充分发挥这个机制的妙用,就可以“同时”完成多个任务了。回到烧水的例子,实际上,无论我在不在厨房,煤气灶总是会把水烧开的,我要做的,只不过是及时关掉煤气灶而已,为了这么一个一秒钟就能完成的动作,却让我死死地守候在厨房里,在10分钟的时间里不停地看壶嘴是不是冒蒸气,怎么说都不划算。我决定安下心来看电视。当然,在有生之年,我都不希望让厨房成为火海,于是我上了闹钟,10分钟以后它会发出“尖叫”,提醒我炉子上的水烧开了,那时我再去关煤气也完全来得及。我用一个中断信号——闹铃——换来了10分钟的欢乐时光,心里不禁由衷地感叹:中断机制真是个好东西。

正是由于中断机制,我才能有条不紊地“同时”完成多个任务,中断机制实质上帮助我提高了并发“处理”能力。它也能给计算机系统带来同样的好处:如果在键盘按下的时候会得到一个中断信号,CPU就不必死守着等待键盘输入了;如果硬盘读写完成后发送一个中断信号,CPU就可以腾出手来集中精力“服务大众”了——无论是人类敲打键盘的指尖还是来回读写介质的磁头,跟CPU的处理速度相比,都太慢了。没有中断机制,就像我们苦守厨房一样,计算机谈不上有什么并行处理能力。

跟人相似,CPU也一样要面对纷繁芜杂的局面——现实中的意外是无处不在的——有可能是用户等得不耐烦,猛敲键盘;有可能是运算中碰到了0除数;还有可能网卡突然接收到了一个新的数据包。这些都需要CPU具体情况具体分析,要么马上处理,要么暂缓响应,要么置之不理。无论如何应对,都需要CPU暂停“手头”的工作,拿出一种对策,只有在响应之后,方能回头完成先前的使命,“把一壶水彻底烧开!”

先让我们感受一下中断机制对并发处理带来的帮助。

让我们用程序来探讨一下烧水问题,如果没有“中断”(注意,我们这里只是模仿中断的场景,实际上是用异步事件——消息——处理机制来展示中断产生的效果。毕竟,在用户空间没有办法与实际中断产生直接联系,不过操作系统为用户空间提供的异步事件机制,可以看作是模仿中断的产物),设计如下:

void StayInKitchen()

{

bool WaterIsBoiled = false;

while ( WaterIsBoiled != true )

{

bool VaporGavenOff = false;

if (VaporGavenOff )

WaterIsBoiled = true;

else

WaterIsBoiled = false;

}

// 关煤气炉

printf(“Close gas oven.\n”);

// 一切安定下来,终于可以看电视了,10分钟的宝贵时间啊,逝者如斯夫…

watching_tv();

return;

}

可以看出,整个流程如同我们前面描述的一样,所有工作要顺序执行,没有办法完成并发任务。



如果用“中断”,在开始烧水的时候设定一个10分钟的“闹铃”,然后让CPU去看电视(有点难度,具体实现不在我们关心的范围之内,留给读者自行解决吧:>)。等闹钟响的时候再去厨房关炉子。

#i nclude

#i nclude

#i nclude

#i nclude

#i nclude



// 闹钟到时会执行此程序

void sig_alarm(int signo)

{

//关煤气炉

printf(“Close gas oven.\n”);

}



void watching_tv()

{

while(1)

{

// 呵呵,悠哉悠哉

}

}



int main()

{

// 点火后设置定时中断

printf(“Start to boil water, set Alarm”);

if (signal( SIGALRM, sig_alrm ) == SIG_ERR)

{

perror("signal(SIGALRM) error");

return -1;

}



// 然后就可以欣赏电视节目了

printf(“Watching TV!\n”);



watching_tv();



return 0;

}



这两段程序都在用户空间执行。第二段程序跟中断也没有太大的关系,实际上它只用了信号机制而已。但是,通过这两个程序的对比,我们可以清楚地看到异步事件的处理机制是如何提升并发处理能力的。

Alarm定时器:alarm相当于系统中的一个定时器,如果我们调用alarm(5),那么5秒钟后就会“响起一个闹铃”(实际上靠信号机制实现的,我们这里不想深入细节,如果你对此很感兴趣,请参考Richard Stevens不朽著作《Unix环境高级编程》)。在闹铃响起的时候会发生什么呢?系统会执行一个函数,至于到底是什么函数,系统允许程序自行决定。程序员编写一个函数,并调用signal对该函数进行注册,这样一旦定时到来,系统就会调用程序员提供的函数(CallBack函数?没错,不过在这里如何实现并不关键,我们就不引入新的概念和细节了)。上面的例子里我们提供的函数是sig_alarm,所做的工作很简单,打印“关闭煤气灶”消息。



上面的两个例子很简单,但很能说明问题,首先,它证明采用异步的消息处理机制可以提高系统的并发处理能力。更重要的是,它揭示了这种处理机制的模式。用户根据需要设计处理程序,并可以将该程序和特定的外部事件绑定起来,在外部事件发生时系统自动调用处理程序,完成相关工作。这种模式给系统带来了统一的管理方法,也带来无尽的功能扩展空间。



计算机系统实现中断机制是非常复杂的一件工作,再怎么说人都是高度智能化的生物,而计算机作为一个铁疙瘩,没有程序的教导就一事无成。而处理一个中断过程,它受到的限制和需要学习的东西太多了。

首先,计算机能够接收的外部信号形式非常有限。中断是由外部的输入引起的,可以说是一种刺激。在烧水的场景中,这些输入是叫声和电视的音乐,我们这里只以声音为例。其实现实世界中能输入人类CPU——大脑的信号很多,图像、气味一样能被我们接受,人的信息接口很完善。而计算机则不然,接受外部信号的途径越多,设计实现就越复杂,代价就越高。因此个人计算机(PC)给所有的外部刺激只留了一种输入方式——特定格式的电信号,并对这种信号的格式、接入方法、响应方法、处理步骤都做了规约(具体内容本文后面部分会继续详解),这种信号就是中断或中断信号,而这一整套机制就是中断机制。

其次,计算机不懂得如何应对信号。人类的大脑可以自行处理外部输入,我从来不用去担心闹钟响时会手足无措——走进厨房关煤气,这简直是天经地义的事情,还用大脑想啊,小腿肚子都知道——可惜计算机不行,没有程序,它就纹丝不动。因此,必须有机制保证外部中断信号到来后,有正确的程序在正确的时候被执行。

还有,计算机不懂得如何保持工作的持续性。我在看电视的时候如果去厨房关了煤气,回来以后能继续将电视进行到底,不受太大的影响。而计算机则不然,如果放下手头的工作直接去处理“意外”的中断,那么它就再也没有办法想起来曾经作过什么,做到什么程度了。自然也就没有什么“重操旧业”的机会了。这样的处理方式就不是并发执行,而是东一榔头,西一棒槌了。

那么,通用的计算机系统是如何解决这些问题的呢?它是靠硬件和软件配合来协同实现中断处理的全过程的。我们将通过Intel X86架构的实现来介绍这一过程。

CPU执行完一条指令后,下一条指令的逻辑地址存放在cs和eip这对寄存器中。在执行新指令前,控制单元会检查在执行前一条指令的过程中是否有中断或异常发生。如果有,控制单元就会抛下指令,进入下面的流程:

1. 确定与中断或异常关联的向量i (0£i£255)

2. 寻找向量对应的处理程序

3. 保存当前的“工作现场”,执行中断或异常的处理程序

4. 处理程序执行完毕后,把控制权交还给控制单元

5. 控制单元恢复现场,返回继续执行原程序

整个流程如下图所示:

 

图一:中断处理过程

让我们深入这个流程,看看都有什么问题需要面对。

1、异常是什么概念?

在处理器执行到由于编程失误而导致的错误指令(例如除数是0)的时候,或者在执行期间出现特殊情况(例如缺页),需要靠操作系统来处理的时候,处理器就会产生一个异常。对大部分处理器体系结构来说,处理异常和处理中断的方式基本是相同的,x86架构的CPU也是如此。异常与中断还是有些区别,异常的产生必须考虑与处理器时钟的同步。实际上,异常往往被称为同步中断。



2、中断向量是什么?

中断向量代表的是中断源——从某种程度上讲,可以看作是中断或异常的类型。中断和异常的种类很多,比如说被0除是一种异常,缺页又是一种异常,网卡会产生中断,声卡也会产生中断,CPU如何区分它们呢?中断向量的概念就是由此引出的,其实它就是一个被送通往CPU数据线的一个整数。CPU给每个IRQ分配了一个类型号,通过这个整数CPU来识别不同类型的中断。这里可能很多朋友会寻问为什么还要弄个中断向量这么麻烦的东东?为什么不直接用IRQ0~IRQ15就完了?比如就让IRQ0为0,IRQ1为1……,这不是要简单得多么?其实这里体现了模块化设计规则,及节约规则。

首先我们先谈谈节约规则,所谓节约规则就是所使用的信号线数越少越好,这样如果每个IRQ都独立使用一根数据线,如IRQ0用0号线,IRQ1用1号线……这样,16个IRQ就会用16根线,这显然是一种浪费。那么也许马上就有朋友会说:那么只用4根线不就行了吗?(2^4=16)。

这个问题,体现了模块设计规则。我们在前面就说过中断有很多类,可能是外部硬件触发,也可能是由软件触发,然而对于CPU来说中断就是中断,只有一种,CPU不用管它到底是由外部硬件触发的还是由运行的软件本身触发的,因为对于CPU来说,中断处理的过程都是一样的:中断现行程序,转到中断服务程序处执行,回到被中断的程序继续执行。CPU总共可以处理256种中断,而并不知道,也不应当让CPU知道这是硬件来的中断还是软件来的中断,这样,就可以使CPU的设计独立于中断控制器的设计,这样CPU所需完成的工作就很单纯了。CPU对于其它的模块只提供了一种接口,这就是256个中断处理向量,也称为中断号。由这些中断控制器自行去使用这256个中断号中的一个与CPU进行交互,比如,硬件中断可以使用前128个号,软件中断使用后128个号,也可以软件中断使用前128个号,硬件中断使用后128个号,这与CPU完全无关了,当你需要处理的时候,只需告诉CPU你用的是哪个中断号就行,而不需告诉CPU你是来自哪儿的中断。这样也方便了以后的扩充,比如现在机器里又加了一片8259芯片,那么这个芯片就可以使用空闲的中断号,看哪一个空闲就使用哪一个,而不是必须要使用第0号,或第1号中断号了。其实这相当于一种映射机制,把IRQ信号映射到不同的中断号上,IRQ的排列或说编号是固定的,但通过改变映射机制,就可以让IRQ映射到不同的中断号,也可以说调用不同的中断服务程序。



3、什么是中断服务程序?

在响应一个特定中断的时候,内核会执行一个函数,该函数叫做中断处理程序(interrupt handler)或中断服务程序(interrupt service routine(ISR))。产生中断的每个设备都有相应的中断处理程序。例如,由一个函数专门处理来自系统时钟的中断,而另外一个函数专门处理由键盘产生的中断。

一般来说,中断服务程序要负责与硬件进行交互,告诉该设备中断已被接收。此外,还需要完成其他相关工作。比如说网络设备的中断服务程序除了要对硬件应答,还要把来自硬件的网络数据包拷贝到内存,对其进行处理后再交给合适的协议栈或应用程序。每个中断服务程序根据其要完成的任务,复杂程度各不相同。

一般来说,一个设备的中断服务程序是它的设备驱动程序(device driver)的一部分——设备驱动程序是用于对设备进行管理的内核代码。



4、隔离变化

不知道您有没有意识到,中断处理前面这部分的设计是何等的简单优美。人是高度智能化的,能够对遇到的各种意外情况做有针对性的处理,计算机相比就差距甚远了,它只能根据预定的程序进行操作。对于计算机来说,硬件支持的,只能是中断这种电信号传播的方式和CPU对这种信号的接收方法,而具体如何处理这个中断,必须得靠操作系统实现。操作系统支持所有事先能够预料到的中断信号,理论上都不存在太大的挑战,但在操作系统安装到计算机设备上以后,肯定会时常有新的外围设备被加入系统,这可能会带来安装系统时根本无法预料的“意外”中断。如何支持这种扩展,是整个系统必须面对的。

而硬件和软件在这里的协作,给我们带来了完美的答案。当新的设备引入新类型的中断时,CPU和操作系统不用关注如何处理它。CPU只负责接收中断信号,并引用中断服务程序;而操作系统提供默认的中断服务——一般来说就是不理会这个信号,返回就可以了——并负责提供接口,让用户通过该接口注册根据设备具体功能而编制的中断服务程序。如果用户注册了对应于一个中断的服务程序,那么CPU就会在该中断到来时调用用户注册的服务程序。这样,在中断来临时系统需要如何操作硬件、如何实现硬件功能这部分工作就完全独立于CPU架构和操作系统的设计了。

而当你需要加入新设备的时候,只需要告诉操作系统该设备占用的中断号、按照操作系统要求的接口格式撰写中断服务程序,用操作系统提供的函数注册该服务程序,设备的中断就被系统支持了。

中断和对中断的处理被解除了耦合。这样,无论是你在需要加入新的中断时,还是在你需要改变现有中断的服务程序时、又或是取消对某个中断支持的时候,CPU架构和操作系统都无需作改变。



5、保存当前工作“现场”

在中断处理完毕后,计算机一般来说还要回头处理原先手头正做的工作。这给中断的概念带来些额外的“内涵”。注一“回头”不是指从头再来重新做,而是要接着刚才的进度继续做。这就需要在处理中断信号之前保留工作“现场”。“现场”这个词比较晦涩,其实就是指一个信息集,它能反映某个时间点上任务的状态,并能保证按照这些信息就能恢复任务到该状态,继续执行下去。再直白一点,现场不过就是一组寄存器值。而如何保护现场和恢复场景是中断机制需要考虑的重点之一。

每个中断处理都要经历这个保存和恢复过程,我们可以抽象出其中的步骤:

1. 保存现场

2. 执行具体的中断服务程序

3. 从中断服务返回

4. 恢复现场

上面说过了,“现场”看似在不断变化,没有哪个瞬间相同。但实际上组成现场的要素却不会有任何改变。也就是说,只要我们保存了相关的寄存器状态,现场就能保存下来。而恢复“现场”就是重新载入这些寄存器。换句话说,对于任何一个中断,保护现场和恢复现场所做的都是完全相同的操作。

既然操作相同,实现操作的过程和代码就相同。减少代码的冗余是模块化设计的基本准则,实在没有道理让所有的中断服务程序都重复实现这样的功能,应该将它作为一种基本的结构由底层的操作系统或硬件完成。而对中断的处理过程需要迅速完成,因此,Intel CPU的控制器就承担了这个任务,非但如此,上面的所有步骤次序都被固化下来,由控制器驱动完成。保存现场和恢复现场都由硬件自动完成,大大减轻了操作系统和设备驱动程序的负担。



6、硬件对中断支持的细节

下面的部分,本来应该介绍8259、中断控制器编程、中断描述符表等内容,可是我看到了潇寒写的“保护模式下的8259A芯片编程及中断处理探究”(见参考资料1),前人之述备矣,读者直接读它好了。
从外而内,Linux对中断的支持

在Linux中,中断处理程序看起来就是普普通通的C函数。只不过这些函数必须按照特定的类型声明,以便内核能够以标准的方式传递处理程序的信息,在其他方面,它们与一般的函数看起来别无二致。中断处理程序与其它内核函数的真正区别在于,中断处理程序是被内核调用来响应中断的,而它们运行于我们称之为中断上下文的特殊上下文中。关于中断上下文,我们将在后面讨论。

中断可能随时发生,因此中断处理程序也就随时可能执行。所以必须保证中断处理程序能够快速执行,这样才能保证尽可能快地恢复被中断代码的执行。因此,尽管对硬件而言,迅速对其中断进行服务非常重要。但对系统的其它部分而言,让中断处理程序在尽可能短的时间内完成执行也同样重要。

即使最精简版的中断服务程序,它也要与硬件进行交互,告诉该设备中断已被接收。但通常我们不能像这样给中断服务程序随意减负,相反,我们要靠它完成大量的其它工作。作为一个例子,我们可以考虑一下网络设备的中断处理程序面临的挑战。该处理程序除了要对硬件应答,还要把来自硬件的网络数据包拷贝到内存,对其进行处理后再交给合适的协议栈或应用程序。显而易见,这种运动量不会太小。



现在我们来分析一下Linux操作系统为了支持中断机制,具体都需要做些什么工作。

首先,操作系统必须保证新的中断能够被支持。计算机系统硬件留给外设的是一个统一的中断信号接口。它固化了中断信号的接入和传递方法,拿PC机来说,中断机制是靠两块8259和CPU协作实现的。外设要做的只是把中断信号发送到8259的某个特定引脚上,这样8259就会为此中断分配一个标识——也就是通常所说的中断向量,通过中断向量,CPU就能够在以中断向量为索引的表——中断向量表——里找到中断服务程序,由它决定具体如何处理中断。(具体细节还请查阅参考资料1,对于为何采用这种机制,该资料有精彩描述)这是硬件规定的机制,软件只能无条件服从。

因此,操作系统对新中断的支持,说简单点,就是维护中断向量表。新的外围设备加入系统,首先得明确自己的中断向量号是多少,还得提供自身中断的服务程序,然后利用Linux的内核调用界面,把〈中断向量号、中断服务程序〉这对信息填写到中断向量表中去。这样CPU在接收到中断信号时就会自动调用中断服务程序了。这种注册操作一般是由设备驱动程序完成的。

其次,操作系统必须提供给程序员简单可靠的编程界面来支持中断。中断的基本流程前面已经讲了,它会打断当前正在进行的工作去执行中断服务程序,然后再回到先前的任务继续执行。这中间有大量需要解决问题:如何保护现场、嵌套中断如何处理等等,操作系统要一一化解。程序员,即使是驱动程序的开发人员,在写中断服务程序的时候也很少需要对被打断的进程心存怜悯。(当然,出于提高系统效率的考虑,编写驱动程序要比编写用户级程序多一些条条框框,谁让我们顶着系统程序员的光环呢?)

操作系统为我们屏蔽了这些与中断相关硬件机制打交道的细节,提供了一套精简的接口,让我们用极为简单的方式实现对实际中断的支持,Linux是怎么完美的做到这一点的呢?



CPU对中断处理的流程:

我们首先必须了解CPU在接收到中断信号时会做什么。没办法,操作系统必须了解硬件的机制,不配合硬件就寸步难行。现在我们假定内核已被初始化,CPU在保护模式下运行。



CPU执行完一条指令后,下一条指令的逻辑地址存放在cs和eip这对寄存器中。在执行新指令前,控制单元会检查在执行前一条指令的过程中是否有中断或异常发生。如果有,控制单元就会抛下指令,进入下面的流程:

1.确定与中断或异常关联的向量i (0£i£255)。

2.籍由idtr寄存器从IDT表中读取第i项(在下面的描述中,我们假定该IDT表项中包含的是一个中断门或一个陷阱门)。

3.从gdtr寄存器获得GDT的基地址,并在GDT表中查找,以读取IDT表项中的选择符所标识的段描述符。这个描述符指定中断或异常处理程序所在段的基地址。



4.确信中断是由授权的(中断)发生源发出的。首先将当前特权级CPL(存放在cs寄存器的低两位)与段描述符(即DPL,存放在GDT中)的描述符特权级比较,如果CPL小于DPL,就产生一个“通用保护”异常,因为中断处理程序的特权不能低于引起中断的程序的特权。对于编程异常,则做进一步的安全检查:比较CPL与处于IDT中的门描述符的DPL,如果DPL小于CPL,就产生一个“通用保护”异常。这最后一个检查可以避免用户应用程序访问特殊的陷阱门或中断门。



5.检查是否发生了特权级的变化,也就是说, CPL是否不同于所选择的段描述符的DPL。如果是,控制单元必须开始使用与新的特权级相关的栈。通过执行以下步骤来做到这点:

a.读tr寄存器,以访问运行进程的TSS段。

b.用与新特权级相关的栈段和栈指针的正确值装载ss和esp寄存器。这些值可以在TSS中找到(参见第三章的“任务状态段”一节)。

c.在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。



6.如果故障已发生,用引起异常的指令地址装载cs和eip寄存器,从而使得这条指令能再次被执行。

7.在栈中保存eflag、cs及eip的内容。

8.如果异常产生了一个硬错误码,则将它保存在栈中。

9.装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量域。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。

控制单元所执行的最后一步就是跳转到中断或者异常处理程序。换句话说,处理完中断信号后,控制单元所执行的指令就是被选中的处理程序的第一条指令。



中断或异常被处理完后,相应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程,这将迫使控制单元:

1.用保存在栈中的值装载cs、eip、或eflag寄存器。如果一个硬错误码曾被压入栈中,并且在eip内容的上面,那么,执行iret指令前必须先弹出这个硬错误码。

2.检查处理程序的CPL是否等于cs中最低两位的值(这意味着被中断的进程与处理程序运行在同一特权级)。如果是,iret终止执行;否则,转入下一步。

3. 从栈中装载ss和esp寄存器,因此,返回到与旧特权级相关的栈。

4. 检查ds、es、fs及gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么,清相应的段寄存器。控制单元这么做是为了禁止用户态的程序(CPL=3)利用内核以前所用的段寄存器(DPL=0)。如果不清这些寄存器,怀有恶意的用户程序就可能利用它们来访问内核地址空间。





再次,操作系统必须保证中断信息能够高效可靠的传递









注一:那么PowerOff(关机)算不算中断呢?如果从字面上讲,肯定符合汉语对中断的定义,但是从信号格式、处理方法等方面来看,就很难符合我们的理解了。Intel怎么说的呢?该中断没有采用通用的中断处理机制。那么到底是不是中断呢?我也说不上来:(



注二:更详细的内容和其它一些注意事项请参考内核源代码包中Documentations/rtc.txt

注三:之所以这里使用汇编而不是C来实现这些函数,是因为C编译器会在函数的实现中推入额外的栈信息。而CPU在中断来临时保存和恢复现场都按照严格的格式进行,一个字节的变化都不能有。



参考资料

1 “保护模式下的8259A芯片编程及中断处理探究” 潇寒 哈工大纯C论坛 http://purec.binghua.com/Article/ShowArticle.asp?ArticleID=91

2 “80x86 IBM PC及兼容计算机(卷I和卷II):汇编语言、设计与接口技术” Muhammad Ali Mazidi等著 张波等译 清华大学出版社

3 “编写操作系统之键盘交互的实现” 潇寒 哈工大纯C论坛 http://purec.binghua.com/Article/ShowArticle.asp?ArticleID=104
 
第 1 张,共 23 张
尚未添加列表。

斌的共享空间

感谢访问!
请稍候...
很抱歉,您输入的评论太长。请缩短您的评论。
您没有输入任何内容,请重试。
很抱歉,我们当前无法添加您的评论。请稍后重试。
若要添加评论,需要您的家长授予您相应权限。请求权限
您的家长禁用了评论功能。
很抱歉,我们当前无法删除您的评论。请稍后重试。
您已超过了一天之内允许提供的评论数上限。请在 24 小时后重试。
因为我们的系统表明您可能在向其他用户提供垃圾评论,您的帐户已禁用了评论功能。如果您认为我们错误地禁用了您的帐户,请联系 Windows Live 支持部门
完成下面的安全检查,您提供评论的过程才能完成。
您在安全检查中键入的字符必须与图片或音频中的字符一致。