版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
如何编写自己的操作系统
2008年10月15日星期三09:51
如何编写自己的操作系统
有人可能担心自己既没有学过计算机原理,也没有学过操作系统原理,更不懂汇编语言,
对C语言也一知半解,能写操作系统吗?答案是没问题。我将带大家一步一步完成自己的操作系统。
当然如果学一学上述内容再好不过。
首先要明确处理器(也就是CPU)控制着计算机。对PC而言,启动的时候,CPU都处在实模式状态,
相当于只是一个Intel8086处理器。也就是说,即使你现在拥有一个奔腾处理器,它的功能也只能
是8086级别。从这一点上来讲,可以使用一些软件把处理器转换到著名的保护模式。只有这样,
我们才可以充分利用处理器的强大功能。
编写操作系统开始是对BIOS控制,取出存储在ROM里的程序。BIOS是用来执行POST(PowerOnSelfTest,
自检)的。自检是检查计算机的完整性(比如外设是否工作正常、键盘是否连接等)。这一切完成以后,
你就会听到PC喇叭发出一声清脆的响声。如果一切正常,BIOS就会选择一个启动设备,
并且读取该设备的第一扇区(即启动扇区),然后控制过程就会转移到指定位置。
启动设备可能是一个软盘、光盘、硬盘,或者其它所选择的设备。在此我们把软盘作为启动设备。
如果我们已经在软盘的启动扇区里写了一些代码,这时它就被执行。因此,我们的目的很明确,
就是往软盘的启动扇区写一些程序。
首先使用8086汇编来写一个小程序,然后将其拷贝至软盘的启动扇区。为了实现拷贝,要写一个C程序。
最后,使用软盘启动计算机。
需要的工具
●as86:这是一个汇编程序,它负责把写的代码转换成目标文件。
●ld86:这是一个连接器,as86产生的目标代码由它来转换成真正的机器语言。
机器语言是8086能够解读的形式。
●GCC:著名的C编程器。因为我们需要写一个C程序将自己的OS转移到软盘中。
●一张空软盘:它用于存储编写的操作系统,也是启动设备。
●一台装有Linux的计算机:这台机器可以很旧,386、486都可以。
在大部分标准Linux发行版中都会带有as86和ld86。在我使用的RedHat7.3中就包含有这两个工具,
并且在默认的情况下,它已经安装在机器里。如果使用的Linux没有这两个工具,可以从网上下载
(http://www.cix.co.uk/~mayday/),这两个工具都包含在一个名为bin86的软件包中。
此外,有关的文档也可以在网上获得(/docs/ldp/howto/Assembly-HOWTO/as86.html)。
开始工作
使用一个你喜欢的编辑器输入以下内容:
entrystart
start:
movax,#0xb800
moves,ax
seges
mov[0],#0x41
seges
mov[1],#0x1f
loop1:jmploop1
这是as86可以读懂的一段汇编程序。第一个句子指明了程序的入口点,声明整个过程从start处开始。
第二行指明了start的位置,说明整个程序要从start处开始执行。0xb800是显存的开始地址。
#表明其后是一个立即数。执行语句:
movax,#oxb800
ax寄存器的值就变为0xb800,这就是显存的地址。下面再将这个值移至es寄存器,es是附加段寄存器。
请记住8086有一个分段的体系结构。它的各段寄存器为代码段、数据段、堆栈段和附加段,
对应的寄存器名称分别为cs、ds、ss和es。事实上,我们把显存地址送入了附加段,因此,
任何送入附加段的东西都会被送到显存中。
要在屏幕上显示字符,就需要向显存中写两个字节。前一个是所要显示字符的ASCⅡ值,
第二个字节表示该字符的属性。属性包括字符的前景色、背景色及是否闪烁等等。
seges指明下一个将要执行的指令是指向es段的。所以,我们把值0x41(在ASCⅡ中表示的字符是A)送到
显存的第一个字节中。接下来要把字符的属性送到下一个字节当中。在此输入的是0x1f,
该属性指的是在蓝色背景下显示白色的字符。因此,如果执行这个程序,
就可以在屏幕上得到显示在蓝底上的一个白色的A。接着是一个循环。
因为在执行完显示字符的任务后,要么让程序结束,要么使用一个循环使其永远运行下去。
把该文件命名为boot.s,然后存盘。
--------------------------------------------------------------------------------
2如何编写自己的操作系统
此处显存的概念说得不是很清楚,有必要进一步解释一下。假设屏幕由80列×25行组成,
那么第一行就需要160字节,其中一个字节用于表示字符,另外一个字节用于表示字符的属性。
如果要在第三行显示某一字符的话,就要跳过显存的第0和1字节(它们是用于显示第1列的),
第2和3字节(它们是用于显示第2列的),然后把需要显示字符的ASCⅡ码值入第4字节,
把字符的属性写入第5字节。
把程序写至启动扇区
下面写一个C程序,把我的操作系统写入软盘第一扇区。程序内容如下:
#include/*unistd.h需要这个文件*/
#include/*包含有read和write函数*/
#include
intmain()
{
charboot_buf[512];
intfloppy_desc,file_desc;
file_desc=open("./boot",O_RDONLY);
read(file_desc,boot_buf,510);
close(file_desc);
boot_buf[510]=0x55;
boot_buf[511]=0xaa;
floppy_desc=open("/dev/fd0",O_RDWR);
lseek(floppy_desc,0,SEEK_CUR);
write(floppy_desc,boot_buf,512);
close(floppy_desc);
}
首先,以只读模式打开boot文件,然后在打开文件时把文件描述符复制到file_desc变量中。
从文件中读取510个字符,或者读取直到文件结束。在本例中由于文件很小,所以是读取至文件结束
。然后关闭文件。
最后4行代码打开软盘驱动设备(一般来说是/dev/fd0)。使用lseek找到文件开始处,
然后从缓冲中向软盘写512个字节。
在read、write、open和lseek的帮助页中,可以看到与函数所有有关的参数及其使用方法。
程序中有两行比较难懂:
boot_buf[510]=0x55;
boot_buf[511]=0xaa;
该信息是用于BIOS的,如果它识别出该设备是一个可启动的设备,那么在第510和511的位置,
该值就应该是0x55和0xaa。程序会把文件boot读至名为boot_buf的缓冲中。
它要求改变第510和第511字节,然后把boot_buf写至软盘之上。如果执行代码,
软盘上的前512字节就包含了启动代码。最后,把文件存为write.c。
编译运行
使用下面的命令把文件变为可执行文件:
as86boot.s-oboot.o
ld86-dboot.o-oboot
ccwrite.c-owrite
首先将boot.s文件编译成目标文件boot.o,然后将该文件连接成最终的boot文件。
最后C程序编译成可执行的write文件。
插入一个空白软盘,运行以下程序:
./write
重新启动电脑,进行BIOS的界面设置,并且把软盘设为第一个启动的设备。然后插入软盘,
电脑从软盘上启动。
启动完成后,在屏幕上可以看到一个字母A(蓝底白字),启动速度很快,几乎是在瞬间完成。
这就意味着系统已经从我们制作的软盘上启动了,并且执行了刚才写入启动扇区的程序。
现在,它正处在一个无限循环的状态。所以,如果想进入Linux,必需拿掉软盘,并且重启机器。
至此,这个操作系统就算完成了,虽然它没有实现什么功能,但是它已经可以启动机器了。
下一期我将在这个启动扇区程序里加入一些代码,使它可以做一些比较复杂的事情
(比如使用BIOS中断、保护模式切换等等)。
自己动手写操作系统(二)
作者:伊梅
上一期,我讲述了如何在软盘的启动扇区写一些代码,然后再从软盘启动的过程。
制作好一个启动扇区,在切换到保护模式之前,我们还应该知道如何使用BIOS中断。
BIOS中断是一些由BIOS提供的、为了使操作系统的创建更容易的低级程序。在本文中,
我们将学习处理BIOS的中断。
为什么要用BIOS
BIOS会把启动扇区拷贝至RAM中,并且执行这些代码。除此之外,BIOS还要做很多其它的事情。
当一个操作系统刚开始启动时,系统中并没有显卡驱动、软盘驱动等任何驱动程序。
因此,启动扇区中不可能包含任何一个驱动程序,我们要采取其它的途径。
这个时候,BIOS就可以帮助我们了。BIOS中包含有各种可以使用的程序,
包括检测安装的设备、控制打印机、计算内存大小等用于各种目的的程序。
这些程序就是所说的BIOS中断。
--------------------------------------------------------------------------------
3如何编写自己的操作系统
如何调用BIOS中断
在一般的程序设计语言中,函数的调用是一件非常容易的事情。比如在C语言中,
如果有一个名为display的程序,它带有两个参数,其中参数noofchar表示显示的字符数,
参数attr表示显示字符的属性。那么要调用它,只需给出程序的名称即可。对于中断的调用
,我们使用的是汇编语言中的int指令。
比如,在C语言中要显示一些东西时,使用的指令如下所示:
display(nofchar,attr);
而使用BIOS时,要实现相同功能使用的指令如下:
int0x10
如何传递参数
在调用BIOS中断之前,我们需要先往寄存器中送一些特定的值。假设要使用BIOS的中断13h,
该中断的功能是把数据从软盘传送至内存之中。在调用该中断之前,要先指定拷贝数据的段地址
,指定驱动器号、磁道号、扇区号,以及要传送的扇区数等等。然后,就要往相应的寄存器送入
相应的值。在进行下面的步骤前,读者有必要对这一点有比较明确地认识。
此外,一个比较重要的事实是同一个中断往往可以实现各种不同的功能。中断所实现的确切功
能取决于所选择的功能号,功能号一般都存在ah寄存器之中。比如中断13h可以用于读磁盘、
写磁盘等功能,如果把3送入ah寄存器中,那么中断选择的功能就是写磁盘;如果把2送入ah寄存
器中,选择的功能则是读磁盘等。
我们要做的事情
这次我们的源代码由两个汇编语言程序和一个C程序组成。第一个汇编文件是引导扇区的代码。
在引导扇区中,我们写的代码是要把软盘中第二扇区拷贝至内存段的0x500处(地址是0x5000,
即偏移地址为0)。这时我们需要使用BIOS的中断13h。这时启动扇区的代码就会把控制权转移至0x500处。
在第二个汇编文件中,代码会使用BIOS中断10h在屏幕上显示一个信息。C程序实现的功能则是把可
执行的文件1拷贝至启动扇区,把可执行的文件2拷贝至软盘的第二扇区。
启动扇区代码
使用中断13h,启动扇区把软盘第二扇区里的内容加载至内存的0x5000处(段地址为0x500)。
下面的代码是用于实现这一目的的代码,将其保存至文件sbect.s中。
LOC1=0x500
entrystart
start:
movax,#LOC1
moves,ax
movbx,#0
movdl,#0
movdh,#0
movch,#0
movcl,#2
moval,#1
movah,#2
int0x13
jmpi0,#LOC1
上面代码第一行类似于一个宏。接下去的两行则是把值0x500加载至es寄存器中,
这是软盘上第二扇区代码将拷贝到的地方(第一扇区是启动扇区)。这时,把段内的偏移设为0。
接下来把驱动器号送入dl寄存器中,其中磁头号送入dl寄存器中,磁道号送入ch寄存器中,
扇区号送入cl寄存器中,扇区数送入al寄存器之中。我们想要实现的功能是把扇区2、磁道号为0、驱动器号为0的内容送至段地址0x500处。所有这些参数都和1.44MB的软盘相对应。
把2送入ah寄存器中,是选择了由中断13h提供的相应功能,即实现从软驱转移数据的功能。
最后调用中断13h,并且转至偏移为0的段地址0x500处。
第二个扇区的代码
第二个扇区中的代码如下所示(把这些代码保存至文件sbect2.s之中):
entrystart
start:
movah,#0x03
xorbh,bh
int0x10
movcx,#26
movbx,#0x0007
movbp,#mymsg
movax,#0x1301
int0x10
loop1:jmploop1
mymsg:
.byte13,10
.ascii"OperatingSystemisLoading......"
上面代码将被加载至段地址为0x500处,并且被执行。在这段代码中,使用了中断10h来获取目前的光标
位置,然后显示信息。
从第3行到第5行用于得到目前光标的位置,在此中断10h选用的是功能3。然后,清除了bh寄存器的内容,
并把字符串送至ch寄存器中。在bx中,我们送入了页码及显示的属性。此处,我们想要在黑背景上显示
白色的字符。然后,把要显示字符的地址送到bp之中,信息由两个字节组成,其值分别为13的10,
它们分别对应回车和LF(换行)的ASCⅡ值。接下来是一个由29个字符组成的串;在下面实现的功能是
输出字符串然后移动光标;最后是调用中断,然后进入循环。
--------------------------------------------------------------------------------
4如何编写自己的操作系统
C程序代码
C程序的源代码如下所示,将其存储为write.c文件。
#include/*unistd.hneedsthis*/
#include/*containsread/write*/
#include
intmain()
{
charboot_buf[512];
intfloppy_desc,file_desc;
file_desc=open("./bsect",O_RDONLY);
read(file_desc,boot_buf,510);
close(file_desc);
boot_buf[510]=0x55;
boot_buf[511]=0xaa;
floppy_desc=open("/dev/fd0",O_RDWR);
lseek(floppy_desc,0,SEEK_SET);
write(floppy_desc,boot_buf,512);
file_desc=open("./sect2",O_RDONLY);
read(file_desc,boot_buf,512);
close(file_desc);
lseek(floppy_desc,512,SEEK_SET);
write(floppy_desc,boot_buf,512);
close(floppy_desc);
}
在上一期中,我曾经介绍过如何操作能启动的软盘。现在这一个过程稍微有点不同,
首先把由bsect.s编译出来的可执行文件bsect拷贝至软盘的启动扇区。然后再把由sect2.s产生的可执行
文件sect2拷贝至软盘的第二个扇区。
把上述文件置于同一目录之下,然后分别对其进行编译,方法如下所示:
as86bsect.s-obsect.o
ld86-dbsect.o-obsect
对sect2.s文件重复以上的操作,得出可执行文件sect2。编译write.c,插入软盘后执行write文件,
命令如下所示:
ccwrite.c-owrite
./write
下一步我们要做的事情
从软盘启动以后,可以看到显示出来的字符串。这是使用了BIOS中断来完成的。
下一期要做的事情是在这个操作系统中实现实模式向保护模式的转换。
自己动手写操作系统(三)
在上两期中(自己动手写操作系统1,2),我向大家讲述了如何使用Linux提供的开发工具在软盘的启动
扇区写一些代码,以及如何调用BIOS的问题。现在,这个操作系统已经越来越接近当年LinusTorvalds的
那个具有"历史意义"的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所示。
当段寄存器被加载的时候,段基地址就会从相应的表入口获得。描述符的内容会被存储在一个程序员不可
见的影像寄存器(shadowregister)之中,以便下一次同一个段可以使用该信息而不用每次都到表中提取
。物理地址由16位或者32位的偏移加上影像寄存器中的基址组成。实模式和保护模式的不同可以从图1
和图2中很清楚地看出来。
--------------------------------------------------------------------------------
5如何编写自己的操作系统
图1实模式的寻址
图2保护模式下的寻址
图3段描述俯的格式
此外,还有一个中断描述符表(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编译器
下面是整个程序的源代码:
org0x07c00;起始地址是0000:7c00
jmpshortbegin_boot;跳过其它的数据,跳转到引导程序的开始处
bootmesgdb"OurOSbootsectorloading......"
pm_mesgdb"Switchingtoprotectedmode...."
dw512;每一扇区的字节数
db1;每一簇的扇区数
dw1;保留的扇区号
db2
dw0x00e0
dw0x0b40
db0x0f0
dw9
dw18
dw2;读写扇区号
dw0;隐藏扇区号
print_mesg:
movah,0x13;使用中断10h的功能13,在屏幕上写一个字符串
moval,0x00;决定调用函数后光标所处的位置
movbx,0x0007;设置显示属性
movcx,0x20;在此字符串长度为32
movdx,0x0000;光标的起始行和列
int0x10;调用BIOS的中断10h
ret;返回调用程序
get_key:
movah,0x00
int0x16;Get_key使用中断16h的功能0,读取下一个字符
ret
clrscr:
movax,0x0600;使用中断10h的功能6,实现卷屏,如果al=0则清屏
movcx,0x0000;清屏
movdx,0x174f;卷屏至23,79
movbh,0;使用颜色0来填充
int0x10;调用10h中断
ret
begin_boot:
callclrscr;先清屏
movbp,bootmesg;提供串地址
callprint_mesg;输出信息
callget_key;等待用户按下任一键
bits16
callclrscr;清屏
movax,0xb800;使gs指向显示内存
movgs,ax;在实模式下显示一个棕色的A
movword[gs:0],0x641;显示
callget_key;调用Get_key等待用户按下任一键
movbp,pm_mesg;设置串指针
callprint_mesg;调用print_mesg子程序
callget_key;等待按键
callclrscr;清屏
cli;关中断
lgdt[gdtr];加载GDT
moveax,cr0
oral,0x01;设置保护模式位
movcr0,eax;将更改后的字送至控制寄存器中
jmpcodesel:go_pm
bits32
go_pm:
movax,datasel
movds,ax;初始化ds和es,使其指向数据段
moves,ax
movax,videosel;初始化gs,使其指向显示内存
movgs,ax
movword[gs:0],0x741;在保护模式下显示一个白色的字符A
spin:jmpspin;循环
bits16
gdtr:
dwgdt_end-gdt-1;gdt的长度
ddgdt;gdt的物理地址
gdt
nullselequ$-gdt;$指向当前位置,所以nullsel=0h
gdt0;空描述符
dd0
dd0;所有的段描述符都是64位的
codeselequ$-gdt;这是8h也就是gdt的第二个描述符
--------------------------------------------------------------------------------
6如何编写自己的操作系统
code_gdt
dw0x0ffff;段描述符的界限是4Gb
dw0x0000
db0x00
db0x09a
db0x0cf
db0x00
dataselequ$-gdt
data_gdt
dw0x0ffff
dw0x0000
db0x00
db0x092
db0x0cf
db0x00
videoselequ$-gdt
dw3999
dw0x8000;基址是0xb8000
db0x0b
db0x92
db0x00
db0x00
gdt_end
times510-($-$$)db0
dw0x0aa55
把上面的代码存在一个名为abc.asm的文件之中,使用命令nasmabc.asm,将得出一个名为abc的文件。
然后插入软盘,输入命令:ddif=abcof=/dev/fd0。该命令将把文件abc写入到软盘的第一扇区之中。
然后重新启动系统,就会看到如下的信息:
*Ourosbooting................
*A(棕色)
*Switchingtoprotectedmode....
*A(白色)
对代码的解释
上面给出了所有的代码,下面我对上述代码做一些解释。
◆使用的函数
下面是代码中一些函数的说明:
print_mesg该子程序使用了BIOS中断10h的功能13h,即向屏幕写一字符串。属性控制是通过向一些寄
存器中送入不同的值来实现的。中断10h是用于各种字符串操作,我们把子功能号13h送到ah中,
用于指明
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025-2026学年统编版七年级道德与法治上学期期末常考题之确立人生目标
- 光刻技术教学课件
- 七下政治考试大题及答案
- 光伏电站实操培训课件
- 某大型化工集团公司导入WCM世界级制造策划资料
- 光伏安全活动培训感想课件
- 光伏发电原理小科普课件
- 2024年全科医学模拟考试题与参考答案
- 侨务知识应知应会
- 2024统编版八年级历史上册期中复习(第1~12课)考点提纲
- 一年级数学重叠问题练习题
- GB/T 29349-2023法庭科学现场照相、录像要求
- 事业单位专业技术人员岗位工资标准表
- 人工湿地施工方案【整编】
- Android图形图像教学课件
- 知识图谱与自然语言处理的深度融合
- 斜拉索无应力索长的计算
- 智慧机场综合安防系统解决方案
- 物业管理理论实务教材
- 2024年高中英语学业水平测试及答案
- 全检员考试试题
评论
0/150
提交评论