说明:
QQ:13916830
email:xieyubo@126.com
欢迎来信指教,如转载请注明作者及出处.下载原文(pdf)
原文:http://purec.binghua.com/Article/Class6/Class7/200411/81.html
注: 此文发表在<<纯C论坛·电子杂志>>2004.10期上
(http://purec.binghua.com/Soft/Class2/dl_hpcem/200410/74.html)
操作系统引导探究 (Version 0.02)
哈尔滨工业大学计算机体系结构实验室 谢煜波(xieyubo@126.com)
Version 0.02修改记录:
对与GDT有关的段描述符方面的描述进行了修订,更正了上一个版本中出现的一些错误,增加了一些描述,使其更完善。
与上个版本中不同的地方均用红色标记。
前言
本篇文章并不旨在完整的讨论一个多引导系统程序怎样去引导不同的操作系统,而只打算从编写操作系统的角度出发,谈谈计算机怎样从加电开始,从无到有,将操作系统运行起来,在其中将尽量详尽的描述从实模式到保护模式的过渡,目的只在于能将所学与广大爱好者共享,为希望开发操作系统的朋友留下一点资料,也为自己留下一点心得。
本篇文章将以开发中的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对中断的处理。
(图二)
由上图(图二)我们可以清楚的看到,当中断信号产生时,中断信号通过“中断地址形成部件”产生一个中断向量地址,此向量地址其实就是指向一个实际内存地址的指针,而这个实际内存地址中往往安排一条跳转指令(jmp)跳转到实际处理此中断的中断服务程序中去执行。这一块专门用于处理中断跳转的内存就被称为中断向量表。在内存中,这块中断向量表被放在什么地方呢?而实际的中断处理程序又在什么地方呢?
3.2 系统的内存安排(1M)
要回答上面的两个问题,我们需要看看系统中内存是怎么安排的。在CPU被加电的时候,最初的1M的内存,是由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就是一个实模式下的内存地址。分号前面的值表示段寄存器中的值,分号后面的值表示偏移量,实际物理地址的形成如下图所示:
(图四)
然而在保护模式下,内存地址却不是如上图所示的方法形成的。那么它又是怎样形成的呢?
4.2 保护模式下的内存地址形成
保护模式下内存地址就复杂多了, 我们首先要分清三个概念:逻辑地址、线性地址与物理地址。物理地址很好理解,逻辑地址也好理解,就是程序所使用的地址。那么什么是线性地址呢?
其实如果不使用分页机制的话,线性地址就是物理地址,它与物理地址是一一对应的,线性地址0,也就是物理地址0。但我们知道,32位的CPU拥有32根地址线,也就是可以访问:
= 4GB的内存空间,这实在是一个太大的空间了!现在很少有机器的物理内存能有这么大。那怎么在有限的物理空间中使用4GB的空间呢?人们把物理内存分成许多页,同样也把整个4GB的线性地址空间分成大小相同的许多页。在线性地址空间中,当某些页被使用的时候,某些页可能没有被使用,操作系统可以让CPU将没有被使用的页调出物理内存(存放在磁盘的某个地方,以备需要的时候再次调入),而把需要使用的页调入,这样,虽然物理内存空间有限,但也几乎可以使用所有的线性地址空间了。这就称为从线性地址到物理地址的映射,这是一个多对一的映射,也就是说多个线性空间中的页对应一个物理空间中的页,希望下面一幅图能有助于你理解这样的分页机制。
(图五)
上面是一种最简单的映射方式,术语称作“直接相连”映射,它大约只能用来说明问题,而在一个实际的操作系统中通常是“全相联相连”映射,也就是说线性地址中的页可以是映射到物理地址中的任何一个页中,只要那块物理地址空间现在是空闲的。不过,通过上图也能说明问题,当线性地址中的页5需要被访问时,CPU通过地址映射机制将其转换到物理地址,发现其对应物理地址中的页1。于是CPU会产生一个所谓的缺页中断来通知操作系统进行处理,操作系统相应这个中断,并在中断服务程序中将物理地址页1中的内容放到磁盘上的一个地方(虚拟内存),然后将线性地址中的页5载入物理内存页1中。这里就当可以比较明显的区别什么是线性地址,什么是物理地址了。
然而,当不使用分页机制的时候,线性地址就会被CPU当做物理地址来使用,线性地址会被直接放在CPU的地址信号线上。不过,在编写应用程序的时候,我们通常使用的却是另一种地址——逻辑地址,从逻辑地址到线性地址也存在着与上述机制类似的一种映射机制,不过这个机制常常称为“段模式”,它是由操作系统与CPU硬件共同完成的。操作系统的任务就是分配映射表,而CPU硬件的任务就是按着映射表进行映射。而这样的映射表在操作系统编写中又称之为“描述符表”,有两种重要的描述符表,一种是“全局描述符表(GDT)”,另一种是“局部描述符表(LDT)”,这两种表的用途不同,但它们的用法却是近似的,下面我们就来描述一下全局描述符表。
说到表,学过数据结构的人都知道,其实它就是一种数据结构,全局描述符表也是一种数据结构,当这种结构放在一块连续的内存之中就称之为表了。表由表项组成,全局描述符表由它的表项“全局描述符”组成,其实单纯的术语就叫“描述符”,只因为它放在全局描述符表中就成了全局描述符了。这个描述符由8个字节组成,下面我们就来看看它的结构:
(图六)
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位组成,下面,我们就来看看它提供了哪些信息:
(图七)
其中:
RPL:指示出特权级,00~11,共0、1、2、3四个特权级,与前述一样。
TI:为0时表明这是个用于全局描述符表的选择子,为1时表明用于局部描述符表。
索引值:用来指示表中第几个描述符。索引值共有13位,因此,每张描述符表共可有8K个表项,而一个表项如前所述,占8个字节,因此一张描述符表最大可达 64K。
不知道大家是否注意到这样一个事实,如果将“段选择子”的最后3位置0,这整个段选择子其实就是一个描述符在描述符表中的偏移量!这里我们可以发现Intel的工程师在设计的时候真的是非常精巧,如此的安排,可以使选取一个描述符的速度极大加快,因为将一个段选择子最后3位清零后与描述符表的基址相加,就立即可以得到一个描述符的物理地址,通过这个地址就可以直接得到一个描述符。那么这个描述符表的基址又是放在哪儿的呢?
所为描述符表的基址也就是此描述符表在内存中的起始地址,也即表中第一个描述符所在的内存地址,系统中用两个特殊的寄存器来存放,一个用于存放全局描述符表的基址,称之为“全局描述符表寄存器(GDTR)”,另一个用来存放局部描述符表的基址,称之为“局部描述符表寄存器(LDTR)”,它们的结构如下图所示:
(图八)
其中表限也即表的大小限制,它的使用与前面所描述的段限是类似的,因此,这里就不在描述了。
在保护模式下,以前实模式下的段寄存器还是有用的,不过它不再用来存放段的基址,而是用来存放“段选择子”,它的名字也变成了“段选择子寄存器”,在访问内存的时候,我们需要给出的是“段选择子”,而不是段基址了。
比如,我现在想使用全局描述符表中第二个表项,即其中的第二个“段描述符”,这个“段选择子”就需按如下的方式构成:
RPL:00,因为我们现在是在写操作系统,工作在0特权级
TI:0,我们使用全局描述符
索引值:1,我们使用第二个全局描述符,第一个全局描述符编号为0,第二个为1
因此,我们的“段选择子”为:0000 0000 0000 10000,也即 0x0008,因此,对于0x0008:0x0000 这样一个逻辑地址,在保护模式下就应看成是使用全局描述符段中第二个描述符所描述的段,并且偏移量为0的内存地址。
这个逻辑地址的线性地址是怎样形成的呢?请看如下的图示:
(图九)
相信,从上图中你可以清楚的看出一个逻辑地址是怎样转换为一个32位的线性地址的。
五、pyos 引导程序编写
pyos是一个正在编写中的操作系统,是一个实验中的项目,关于编写的目的与动机我已在前言中谈论过了,这里,仅就此篇所讲述的内容,谈谈pyos引导程序的编写,在编写期间参考了linux 0.11 内核引导程序的编写,不过pyos并不是基于linux的,就它们的引导程序之间也有许多不一样的地方。下面我们先来看看pyos的整个引导区的内存安排:
(图十)
上面一幅图就是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)
3. 《计算机组成原理》(唐朔飞,高等教育出版社)
【全文完·责任编辑:iamxiaohan@hitbbs】
注: 此文发表在<<纯C论坛·电子杂志>>2004.10期上
(http://purec.binghua.com/Soft/Class2/dl_hpcem/200410/74.html)