关闭

自己动手写操作系统

123人阅读 评论(0) 收藏 举报

有人可能担心自己既没有学过计算机原理,也没有学过操作系统原理,更不懂 汇编语言 ,对 C语言 也一知半解,能写操作系统吗?答案是没问题。我将带大家一步一步完成自己的操作系统。当然如果学一学上述内容再好不过。
  
  首先要明确处理器(也就是CPU)控制着计算机。对PC而言,启动的时候,CPU都处在实模式状态,相当于只是一个Intel 8086处理器。也就是说,即使你现在拥有一个奔腾处理器,它的功能也只能是8086级别。从这一点上来讲,可以使用一些软件把处理器转换到著名的保护模式。只有这样,我们才可以充分利用处理器的强大功能。
  
  编写操作系统开始是对BIOS控制,取出存储在ROM里的程序。BIOS是用来执行POST(Power On Self Test,自检)的。自检是检查计算机的完整性(比如外设是否工作正常、键盘是否连接等)。这一切完成以后,你就会听到PC喇叭发出一声清脆的响声。如果一切正常,BIOS就会选择一个启动设备,并且读取该设备的第一扇区(即启动扇区),然后控制过程就会转移到指定位置。启动设备可能是一个软盘、光盘、硬盘,或者其它所选择的设备。在此我们把软盘作为启动设备。如果我们已经在软盘的启动扇区里写了一些代码,这时它就被执行。因此,我们的目的很明确,就是往软盘的启动扇区写一些程序。
  
  首先使用8086汇编来写一个小程序,然后将其拷贝至软盘的启动扇区。为了实现拷贝,要写一个C程序。最后,使用软盘启动计算机。
  
  需要的工具
  
  ● as86:这是一个汇编程序,它负责把写的代码转换成目标文件。
  
  ● ld86:这是一个连接器,as86产生的目标代码由它来转换成真正的机器语言。机器语言是8086能够解读的形式。
  
  ● GCC:著名的C编程器。因为我们需要写一个C程序将自己的OS转移到软盘中。
  
  一张空软盘:它用于存储编写的操作系统,也是启动设备。
  
  一台装有 Linux 的计算机:这台机器可以很旧,386486都可以。
  
  在大部分标准Linux发行版中都会带有as86ld86。在我使用的Red Hat 7.3中就包含有这两个工具,并且在默认的情况下,它已经安装在机器里。如果使用的Linux没有这两个工具,可以从网上下载(http://www.cix.co.uk/~mayday/),这两个工具都包含在一个名为bin86的软件包中。此外,有关的文档也可以在网上获得(www.linux.org/docs/ldp/howto/Assembly-HOWTO/as86.html)
  
  开始工作
  
  使用一个你喜欢的编辑器输入以下内容:
  
  entry start
  start:
   mov ax,#0xb800
   mov es,ax
   seg es
   mov [0],#0x41
   seg es
   mov [1],#0x1f
  loop1: jmp loop1
  
  这是as86可以读懂的一段汇编程序。第一个句子指明了程序的入口点,声明整个过程从start处开始。第二行指明了start的位置,说明整个程序要从start处开始执行。0xb800是显存的开始地址。#表明其后是一个立即数。执行语句:
  
  mov ax,#oxb800
  
  ax寄存器的值就变为0xb800,这就是显存的地址。下面再将这个值移至es寄存器,es是附加段寄存器。请记住8086有一个分段的体系结构。它的各段寄存器为代码段、数据段、堆栈段和附加段,对应的寄存器名称分别为csdssses。事实上,我们把显存地址送入了附加段,因此,任何送入附加段的东西都会被送到显存中。
  
  要在屏幕上显示字符,就需要向显存中写两个字节。前一个是所要显示字符的ASCⅡ值,第二个字节表示该字符的属性。属性包括字符的前景色、背景色及是否闪烁等等。seg es指明下一个将要执行的指令是指向es段的。所以,我们把值0x41(ASCⅡ中表示的字符是A)送到显存的第一个字节中。接下来要把字符的属性送到下一个字节当中。在此输入的是0x1f,该属性指的是在蓝色背景下显示白色的字符。因此,如果执行这个程序,就可以在屏幕上得到显示在蓝底上的一个白色的A。接着是一个循环。因为在执行完显示字符的任务后,要么让程序结束,要么使用一个循环使其永远运行下去。把该文件命名为boot.s,然后存盘。
  
  此处显存的概念说得不是很清楚,有必要进一步解释一下。假设屏幕由80×25行组成,那么第一行就需要160字节,其中一个字节用于表示字符,另外一个字节用于表示字符的属性。如果要在第三行显示某一字符的话,就要跳过显存的第01字节(它们是用于显示第1列的),第23字节(它们是用于显示第2列的),然后把需要显示字符的ASCⅡ码值入第4字节,把字符的属性写入第5字节。
  
  把程序写至启动扇区
  
  下面写一个C程序,把我的操作系统写入软盘第一扇区。程序内容如下:
  
  #include <sys/types.h> /* unistd.h 需要这个文件 */
  #include /* 包含有readwrite函数 */
  #include
  int main()
  {
   char boot_buf[512];
   int floppy_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个字节。
  
  在readwriteopenlseek的帮助页中,可以看到与函数所有有关的参数及其使用方法。程序中有两行比较难懂:
  
  boot_buf[510] = 0x55;
  boot_buf[511] = 0xaa;
  
  该信息是用于BIOS的,如果它识别出该设备是一个可启动的设备,那么在第510511的位置,该值就应该是0x550xaa。程序会把文件boot读至名为boot_buf的缓冲中。它要求改变第510和第511字节,然后把boot_buf写至软盘之上。如果执行代码,软盘上的前512字节就包含了启动代码。最后,把文件存为write.c
  
  编译运行
  
  使用下面的命令把文件变为可 执行文件 :
  
  as86 boot.s -o boot.o
  ld86 -d boot.o -o boot
  cc write.c -o write
  
  首先将boot.s文件编译成目标文件boot.o,然后将该文件连接成最终的boot文件。最后C程序编译成可执行的write文件。
  
  插入一个空白软盘,运行以下程序:
  
  ./write
  
  重新启动电脑,进行BIOS的界面设置,并且把软盘设为第一个启动的设备。然后插入软盘,电脑从软盘上启动。
  
  启动完成后,在屏幕上可以看到一个字母A(蓝底白字),启动速度很快,几乎是在瞬间完成。这就意味着系统已经从我们制作的软盘上启动了,并且执行了刚才写入启动扇区的程序。现在,它正处在一个无限循环的状态。所以,如果想进入Linux,必需拿掉软盘,并且重启机器。
  
  至此,这个操作系统就算完成了,虽然它没有实现什么功能,但是它已经可以启动机器了。
  
  

阅读:4

自己动手写操作系统()

 

 

[字体: ]


  上一期,我讲述了如何在软盘的启动扇区写一些代码,然后再从软盘启动的过程。制作好一个启动扇区,在切换到保护模式之前,我们还应该知道如何使用BIOS中断。BIOS中断是一些由BIOS提供的、为了使操作系统 的创建更容易的低级程序。在本文中,我们将学习处理BIOS的中断。
  
  为什么要用BIOS
  
  BIOS会把启动扇区拷贝至RAM中,并且执行这些代码。除此之外,BIOS还要做很多其它的事情。当一个操作系统刚开始启动时,系统中并没有显卡驱动、软盘驱动等任何驱动程序。因此,启动扇区中不可能包含任何一个驱动程序,我们要采取其它的途径。这个时候,BIOS就可以帮助我们了。BIOS中包含有各种可以使用的程序,包括检测安装的设备、控制打印机、计算内存大小等用于各种目的的程序。这些程序就是所说的BIOS中断。
  
  如何调用BIOS中断
  
  在一般的程序设计语言中,函数的调用是一件非常容易的事情。比如在 C语言 中,如果有一个名为display的程序,它带有两个参数,其中参数noofchar表示显示的字符数,参数attr表示显示字符的属性。那么要调用它,只需给出程序的名称即可。对于中断的调用,我们使用的是 汇编语言 中的int指令。
  
  比如,在C语言中要显示一些东西时,使用的指令如下所示:
  
  display(nofcharattr)
  
  而使用BIOS时,要实现相同功能使用的指令如下:
  
  int 0x10
  
  如何传递参数
  
  在调用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
  entry start
  start:
   mov ax,#LOC1
   mov es,ax
   mov bx,#0
   mov dl,#0
   mov dh,#0
   mov ch,#0
   mov cl,#2
   mov al,#1
   mov ah,#2
   int 0x13
   jmpi 0,#LOC1
  
  上面代码第一行类似于一个宏。接下去的两行则是把值0x500加载至es寄存器中,这是软盘上第二扇区代码将拷贝到的地方(第一扇区是启动扇区)。这时,把段内的偏移设为0
  
  接下来把驱动器号送入dl寄存器中,其中磁头号送入dl寄存器中,磁道号送入ch寄存器中,扇区号送入cl寄存器中,扇区数送入al寄存器之中。我们想要实现的功能是把扇区2、磁道号为0、驱动器号为0的内容送至段地址0x500处。所有这些参数都和1.44MB的软盘相对应。
  
  把2送入ah寄存器中,是选择了由中断13h提供的相应功能,即实现从软驱转移数据的功能。
  
  最后调用中断13h,并且转至偏移为0的段地址0x500处。
  
  第二个扇区的代码
  
  第二个扇区中的代码如下所示(把这些代码保存至文件sbect2.s之中)
  
  entry start
  start:
   mov ah,#0x03
   xor bh,bh
   int 0x10
  
   mov cx,#26
   mov bx,#0x0007
   mov bp,#mymsg
   mov ax,#0x1301
   int 0x10
  
  loop1: jmp loop1
  mymsg:
   .byte 13,10
   .ascii “Operating System is Loading......”
  
  上面代码将被加载至段地址为0x500处,并且被执行。在这段代码中,使用了中断10h来获取目前的光标位置,然后显示信息。
  
  从第3行到第5行用于得到目前光标的位置,在此中断10h选用的是功能3。然后,清除了bh寄存器的内容,并把字符串送至ch寄存器中。在bx中,我们送入了页码及显示的属性。此处,我们想要在黑背景上显示白色的字符。然后,把要显示字符的地址送到bp之中,信息由两个字节组成,其值分别为1310,它们分别对应回车和LF(换行)ASCⅡ值。接下来是一个由29个字符组成的串;在下面实现的功能是输出字符串然后移动光标;最后是调用中断,然后进入循环。
  
  C程序代码
  
  C程序的源代码如下所示,将其存储为write.c文件。
  
  #include /* unistd.h needs this */
  #include /* contains read/write */
  #include
  int main()
  {
   char boot_buf[512];
   int floppy_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拷贝至软盘的第二个扇区。
  
  把上述文件置于同一目录之下,然后分别对其进行编译,方法如下所示:
  
  as86 bsect.s -o bsect.o
  ld86 -d bsect.o -o bsect
  
  对sect2.s文件重复以上的操作,得出可执行文件sect2。编译write.c,插入软盘后执行write文件,命令如下所示:
  
  cc write.c -o write
  ./write
  
  下一步我们要做的事情
  
  从软盘启动以后,可以看到显示出来的字符串。这是使用了BIOS中断来完成的。下一期要做的事情是在这个操作系统中实现实模式向保护模式的转换。

自己动手写操作系统(三)

 

 

[字体: ]


  在上两期中(自己动手写操作系统12),我向大家讲述了如何使用 Linux 提供的开发工具在软盘的启动扇区写一些代码,以及如何调用BIOS的问题。现在,这个操作系统已经越来越接近当年Linus Torvalds的那个具有历史意义 Linux内核了。 因此,要马上把这个系统切换到保护模式之下。
  
  什么是保护模式
  
  自从1969年推出第一个微处理器以来,Intel处理器就在不断地更新换代,从8086808880286,到8038680486、奔腾、奔腾、奔腾4等,其体系结构也在不断变化。80386以后,提供了一些新的功能,弥补了8086的一些缺陷。这其中包括内存保护、多任务及使用640KB以上的内存等,并仍然保持和8086家族的兼容性。也就是说80386仍然具备了808680286的所有功能,但是在功能上有了很大的增强。早期的处理器是工作在实模式之下的,80286以后引入了保护模式,而在80386以后保护模式又进行了很大的改进。在80386中,保护模式为程序员提供了更好的保护,提供了更多的内存。事实上,保护模式的目的不是为了保护程序,而是要保护程序以外的所有程序(包括操作系统)。
  
  简言之,保护模式是处理器的一种最自然的模式。在这种模式下,处理器的所有指令及体系结构的所有特色都是可用的,并且能够达到最高的性能。
  
  保护模式和实模式
  
  从表面上看,保护模式和实模式并没有太大的区别,二者都使用了内存段、中断和设备驱动来处理硬件,但二者有很多不同之处。我们知道,在实模式中内存被划分成段,每个段的大小为64KB,而这样的段地址可以用16位来表示。内存段的处理是通过和段寄存器相关联的内部机制来处理的,这些段寄存器(CSDSSSES)的内容形成了 物理地址 的一部分。具体来说,最终的物理地址是由16位的段地址和16位的段内偏移地址组成的。用公式表示为:
  
  物理地址=左移4位的段地址+偏移地址。
  
  在保护模式下,段是通过一系列被称之为描述符表的表所定义的。段寄存器存储的是指向这些表的指针。用于定义内存段的表有两种:全局描述符表(GDT)和局部描述符表(LDT)GDT是一个段描述符数组,其中包含所有应用程序都可以使用的基本描述符。在实模式中,段长是固定的(64KB),而在保护模式中,段长是可变的,其最大可达4GBLDT也是段描述符的一个数组。与GDT不同,LDT是一个段,其中存放的是局部的、不需要全局共享的段描述符。每一个操作系统都必须定义一个GDT,而每一个正在运行的任务都会有一个相应的LDT。每一个描述符的长度是8个字节,格式如图3所示。当段寄存器被加载的时候,段基地址就会从相应的表入口获得。描述符的内容会被存储在一个程序员不可见的影像寄存器(shadow register)之中,以便下一次同一个段可以使用该信息而不用每次都到表中提取。物理地址由16位或者32位的偏移加上影像寄存器中的基址组成。实模式和保护模式的不同可以从图1和图2中很清楚地看出来。
    
  图1 实模式的寻址
   
  图2 保护模式下的寻址
  
  图3 段描述俯的格式
  
  此外,还有一个中断描述符表(IDT)。这些中断描述符会告诉处理器到那里可以找到中断处理程序。和实模式一样,每一个中断都有一个入口,但是这些入口的格式却完全不同。因为在切换到保护模式的过程中没有使用到IDT,所以在此就不多做介绍了。
  
  进入保护模式
  
  80386432位控制寄存器,名字分别为CR0CR1CR2CR3CR1是保留在未来处理器中使用的,在80386中没有定义。CR0包含系统的控制标志,用于控制处理器的操作模式和状态。CR2CR3是用于控制分页机制的。在此,我们关注的是CR0寄存器的PE位控制,它负责实模式和保护模式之间的切换。当PE=1时,说明处理器运行于保护模式之下,其采用的段机制和前面所述的相应内容对应。如果PE=0,那么处理器就工作在实模式之下。
  
  切换到保护模式,实际就是把PE位置为1。为了把系统切换到保护模式,还要做一些其它的事情。程序必须要对系统的段寄存器和控制寄存器进行初始化。把PE位置1后,还要执行跳转指令。过程简述如下:
  
  1.创建GDT;
  
  2.通过置PE位为1进入保护模式;
  
  3.执行跳转以清除在实模式下读取的任何指令。
  
  下面使用代码来实现这个切换过程。
  
  需要的东西
  
  一张空白软盘
  
  ◆ NASM编译器
  
  下面是整个程序的源代码:
  
  org 0x07c00; 起始地址是0000:7c00
  jmp short begin_boot  ; 跳过其它的数据,跳转到引导程序的开始处
  bootmesg db "Our OS boot sector loading ......"
  pm_mesg db "Switching to protected mode ...."
  dw 512 ; 每一扇区的字节数
  db 1 ; 每一簇的扇区数
  dw 1 ; 保留的扇区号
  db 2   
  dw 0x00e0   
  dw 0x0b40   
  db 0x0f0   
  dw 9   
  dw 18   
  dw 2 ; 读写扇区号
  dw 0 ; 隐藏扇区号
  print_mesg :
  mov ah,0x13 ; 使用中断10h的功能13,在屏幕上写一个字符串
  mov al,0x00 ; 决定调用函数后光标所处的位置
  mov bx,0x0007  ; 设置显示属性
  mov cx,0x20 ; 在此字符串长度为32
  mov dx,0x0000  ; 光标的起始行和列
  int 0x10 ; 调用BIOS的中断10h
  ret ; 返回调用程序
  get_key :
  mov ah,0x00  
  int 0x16 ; Get_key使用中断16h的功能0,读取下一个字符
  ret
  clrscr :
  mov ax,0x0600 ; 使用中断10h的功能6,实现卷屏,如果al=0则清屏
  mov cx,0x0000 ; 清屏
  mov dx,0x174f ; 卷屏至2379
  mov bh,0 ; 使用颜色0来填充
  int 0x10 ; 调用10h中断
  ret
  begin_boot :
  call clrscr  ; 先清屏
  mov bp,bootmesg ; 提供串地址
  call print_mesg ; 输出信息
  call get_key  ; 等待用户按下任一键
  bits 16
  call clrscr ; 清屏
  mov ax,0xb800 ; 使gs指向显示内存
  mov gs,ax ; 在实模式下显示一个棕色的A
  mov word [gs:0],0x641 ; 显示
  call get_key ; 调用Get_key等待用户按下任一键
  mov bp,pm_mesg ; 设置串指针
  call print_mesg ; 调用print_mesg子程序
  call get_key ; 等待按键
  call clrscr ; 清屏
  cli ; 关中断
  lgdt[gdtr] ; 加载GDT
  mov eax,cr0   
  or al,0x01 ; 设置保护模式位
  mov cr0,eax ; 将更改后的字送至控制寄存器中
  jmp codesel:go_pm
  bits 32
  go_pm :
  mov ax,datasel 
  mov ds,ax ; 初始化dses,使其指向数据段
  mov es,ax
  mov ax,videosel ; 初始化gs,使其指向显示内存
  mov gs,ax
  mov word [gs:0],0x741 ; 在保护模式下显示一个白色的字符A
  spin : jmp spin ; 循环
  bits 16
  gdtr :
  dw gdt_end-gdt-1 ; gdt的长度
  dd gdt ; gdt的物理地址
  gdt
  nullsel equ $-gdt ; $指向当前位置,所以nullsel = 0h
  gdt0 ; 空描述符
  dd 0    
  dd 0 ; 所有的段描述符都是64位的
  codesel equ $-gdt ; 这是8h也就是gdt的第二个描述符
  code_gdt    
  dw 0x0ffff ; 段描述符的界限是4Gb
  dw 0x0000    
  db 0x00      
  db 0x09a  
  db 0x0cf  
  db 0x00  
  datasel equ $-gdt  
  data_gdt    
  dw 0x0ffff    
  dw 0x0000    
  db 0x00      
  db 0x092
  db 0x0cf
  db 0x00
  videosel equ $-gdt   
  dw 3999    
  dw 0x8000 ; 基址是0xb8000
  db 0x0b
  db 0x92    
  db 0x00    
  db 0x00
  gdt_end
  times 510-($-$$) db 0 
  dw 0x0aa55
  
  把上面的代码存在一个名为abc.asm的文件之中,使用命令nasm abc.asm,将得出一个名为abc的文件。然后插入软盘,输入命令:dd if=abc of=/dev/fd0。该命令将把文件abc写入到软盘的第一扇区之中。然后重新启动系统,就会看到如下的信息:
  
  *Our os booting................
  * A (棕色)
  * Switching to protected mode....
  * A (白色)
  
  对代码的解释
  
  上面给出了所有的代码,下面我对上述代码做一些解释。
  
  使用的函数
  
  下面是代码中一些函数的说明:
  
  print_mesg 该子程序使用了BIOS中断10h的功能13h,即向屏幕写一字符串。属性控制是通过向一些寄存器中送入不同的值来实现的。中断10h是用于各种字符串操作,我们把子功能号13h送到ah中,用于指明要打印一个字符串。al寄存器中的0说明了光标返回的起始位置,0表示调用函数后光标返回到下一行的行首。如果al1则表示光标位于最后一个字符处。
  
  显存被分成了几页,在同一时转载

 

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:515次
    • 积分:25
    • 等级:
    • 排名:千里之外
    • 原创:2篇
    • 转载:0篇
    • 译文:0篇
    • 评论:0条
    文章分类
    文章存档