LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 1427|回复: 7

Writing your own tony os-附原文下载

[复制链接]
发表于 2007-11-12 10:04:51 | 显示全部楼层 |阅读模式
Writing your own tony os
By
Krishnakumar R
Raghu and Chitkala

Part I
前言
这篇文章是为手动建立一个引导程序所编制的指导.第一部分介绍在开机之后过程的理论.这也能解释我们的计划.第二部分介绍了在开始之前你需要得到的东西,第三则是实际的程序.我们小小的程序并不能真正的引导linux,但是它可以显示器上显示一些东西.

1 背景
1.1 迷人的东西
处理器控制着计算机.在启动的时候,每个处理器都只是一个8086芯片.即使你使用的是最新奔腾,它也只有8086的性能.从这点上看,我们可以把处理器弄到声名狼藉的保护模式下运行一些软件.只有这样我们才能发挥出处理器的全部能力.

1.2 我们的角色
在初始化的时候,控制权在BIOS.这只是一组存放在ROM中的一组程序.BIOS执行开机自我检测.它检查集成在计算机上的部件是否工作(比如键盘是不是连接上了).这是在你开机后听到锋鸣器响的时候.如果所有检测都通过,BIOS选择一个启动设备.它会把设备的第一个扇区的内容拷贝到内存0x7c00的处.然后把控制权转交到这里.启动设备可能是软盘,cdrom,硬盘或者其它你所选择的设备.这里,我们将使用软盘作为启动设备.如果我们在软盘的启动扇区写入了一些代码,它就会执行.我们的角色很清楚:就是写一些程序到软盘的启动扇区.

1.3 计划
首先用8086汇编写一个小程序(别害怕,我会教你怎么写),然后把它写入到软盘的启动扇区中.为了进行拷贝,我们需要写一个C程序.用这个软盘来启动计算机,好好享受吧.

2 你应该有的东西
As86
    这是汇编器,用这个工具可以把我们的汇编代码转换为目标文件.
Ld86
    这是链接器.as86产生的目标代码用这个工具可以转换为真正的机器语言.机器语言将使用标准的8086格式.
Gcc
    C编译器.我们需要一个C程序将我们的系统拷贝到软盘上.
一个空的软盘
这个软盘将用于存储我们的系统.它也是我们的启动设备.
好用的老版本的linux
你应该知道这是作什么的.

As86和ld86可以在大多数和发行版中找到.如果没有,你也可以从”www.cix.co.uk/~mayday”下载.
它们都在一个叫bin86的目录里.在www.linux.org/docs/ldp/howto/assemby-howto/as86.html有很好的文档可以参考.

3 1,2,3 开始!
3.1 引导片断
找个你喜欢的编辑器,然后把下面的代码敲进去.
入口点
  1. start:
  2.       mov ax,#0xb800
  3.       mov es,ax
  4.       seg es
  5.       mov [0],#0x41
  6.       seg es
  7.       mov [1],#0x1f
  8. loop1: jmp loop1
复制代码

这是一段as86可以理解的汇编代码.第一段指明了程序的入口点.我们期待在初始化阶段,控制权会转移到这个标签处(别忘了在start后加上”:”).第二行代码描述标签start的地址.这段程序最先被执行的就是标签start之后的那一条.
0xb800是显存地址.#号代表一个立即数.在执行了
   
  1. mov ax,#0xb800
复制代码

之后,ax寄存器会存储0xb800这个值.也就是显存的地址.现在我们将这值转移到es寄存器,es表示扩展寄存器.要知道8086拥有数个段寄存器.比如代码段,数据段,扩展段等等.这也是cs,ds和es的由来.实际上,我们将显示地址放进扩展段后,所有写入扩展段的数据都将进入显存.
为了在显示器上打印出一个字符,需要向显存写入两个字节.第一个就是你要写入字符的ascii码,第二个则是字符的属性.属性指出了字符的前景色,背景色,及是否要闪烁等内容.(seg es is is actually a prefix that tells which instruction is to be executed next with reference to es segment.).所以,我们将字符A的ascii码值0x41挪到显存的第一个字节中.然后我们就需要将字符的属性挪到显存的下一个字节当中去.我们键入0x1f,代表蓝色背景色和白色字符.所以如果我们运行这个程序,就会看见在蓝色的屏幕上有一个白色的字符A.然后是一个循环.在显示了字符之后我们要么停止执行这个程序,要么让它永远循环下去.将这个文件保存为boot.s.
空闲的显存也许不会很干净,这个我会在晚些时候再解释.我假设屏幕有25行,80列.每一行需要160个字节,一半是字符,一半是字符属性.如果我们要在第三列写一些字符,那么我们需要跳过第0和第1个字节,因为它们是第一个列的字符,跳过第2,3个字节,因为它们是第二列的字符,然后在显存的第4个字节上写入ascii码,在第5个字节上写入它的属性.

3.2 将引导程序写入软盘
我们需要一个c程序来将我们的代码(OS 代码)写入软盘的第一个扇区.下面是代码:
  1. #include <sys/types.h> /* unistd.h needs this */
  2. #include <unistd.h>    /* contains read/write */
  3. #include <fcntl.h>

  4. int main()
  5. {
  6.         char boot_buf[512];
  7.         int floppy_desc, file_desc;
  8.   
  9.          
  10.         file_desc = open("./boot", O_RDONLY);
  11.         read(file_desc, boot_buf, 510);
  12.         close(file_desc);
  13.          
  14.         boot_buf[510] = 0x55;
  15.         boot_buf[511] = 0xaa;

  16.         floppy_desc = open("/dev/fd0", O_RDWR);
  17.         lseek(floppy_desc, 0, SEEK_CUR);
  18.         write(floppy_desc, boot_buf, 512);
  19.         close(floppy_desc);
  20. }
复制代码

首先我们以只读属性将文件boot打开,然后将打开文件的文件描述符拷贝到变量file_desc中.读出文件的前510个字节,或者(如果文件没有这么大就)读入所有的内容.因为这个程序非常小,所以会是后一种情况.然后就关闭这个文件.
最后的四行是打开软盘.它使用lseek函数将指针定位到文件的开始处,然后将boot_buf内的512个字节写入软盘.函数read,write,open和lseek的文档(参考man 2)会给出足够的信息让你理解它们的参数是如何使用的.在中间的部分有两行代码,看上去有点神奇:
  1. boot_buf[510] = 0x55;
  2. boot_buf[511] = 0xaa;
复制代码

这是给BIOS的信息.如果BIOS认定一个设备是启动设备,那么这个设备应该在它的第510和511个字节处的值为0x55和0xaa.现在我们完成了.这个程序将名为boot的文件读入名为boot_buf的缓冲区.然后它改变了第510和511个字节的值后又将这些内容写入到软盘中.如果我们执行这个程序,软盘的前512个字节就包含了我们的引导程序.将文件保存为write.c.

3.3 咱们动手吧
要想做出可执行的东西来,你要在linux的提示符下面打出下面的命令:
  1. as86 boot.s –o boot.o
  2. ld86 –d boot.o –o boot
  3. cc write.c –o write
复制代码

首先是将汇编文件编译成一个目标文件boot.o.然后再将boot.o链接成为最终的文件boot. Ld86的-d选项是要移除所有的头文件并产生纯净的二进制.读一下as86和ld86的手册就可以解决所有的疑问.然后我们编译C程序,使它生成一个名为write的文件.
插入一张空的软盘,然后输入:
  1. ./write
复制代码

重启你的机器,进入BIOS将软盘设置为启动设备.将软盘插入软驱然后看着计算机从软盘启动.
你会看到一个’A’(白色的前景色,蓝色的背景色).这代表我们制作的软盘已经成功的启动了计算机,我们编制的引导程序已经在执行.它现在正在我们写在引导程序最后的代码中无限循环.我们现在要重新启动计算机,并拿出软盘以便让电脑进入linux.
从这里开始,我们要在我们的引导程序中插入更多的代码,证它做一些更为复杂的事,比如使用BIOS中断,进行保护模式切换等.后面的章节会指引你做更多的改进,继续吧!

注:看到这里,我真庆幸当时不顾别的意见给自己的机器安装了软驱.我知道要做上面的实验即使没有软驱也一样可以,但是会浪费一些时间,呵呵.看来硬件是不是过时,是不是太过于古老,要看你想要它来做什么.

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
 楼主| 发表于 2007-11-12 10:08:40 | 显示全部楼层
PART II
下面是每一个学习了如何制作一个引导程序的人在进行保护模式切换之前都应该知道的,就是如何使用BIOS中断.BIOS中断是由BIOS提供的,为了操作系统的编写者更轻松的一种底层的特殊程序.这部分的文篇就是专门针对BIOS中断的.

1 理论
1.1 为什么是BIOS?
BIOS承担了将引导程序从启动扇区拷贝到RAM,并且从RAM执行代码的作用.除此以外,BIOS还做了一些别的事情.当操作系统刚刚启动的时候,它并没有显卡驱动,软盘驱动或是其它任何一种驱动.要将这些驱动写入引导程序几乎是不可能的.所以就需要一些其它的办法.BIOS的作用就在这里显现出来了.BIOS包含了很多可以使用的例程.比如现在已经有的很多的各种目的,比如检查设备是否被安装,控制打印机,查看内存容量等.这些例程,我们就称之为BIOS中断.

1.2 我们如何调用中断?
一般而言我们在程序语言调用这些例程。以C语言为例,有一个函数名为display,有下面这些参数:noofchar是要显示的字符的个数,attr是要显示的字符的属性。在这里我们就使用了中断,因为我们使用了汇编语言中的int指令。
比如我要用c语句在屏幕上打印些东西,就要像下面这样:

  1. Display(noofchar, attr);
复制代码


就像这样,当我们使用BIOS的时候,这样写:

  1. Int 0x10
复制代码


1.3 怎么传递参数?
在调用BIOS中断之前,我们需要在寄存器中载入特定格式的值。假设我们要使用13号BIOS中断,可以把数据从软盘拷贝到内存中。在调用13号中断之前,我们要指定要拷贝的数据的段地址。同样,我们也得以参数的形式指定驱动器号,磁道号,扇区号,要拷贝的扇区的数量等等。我们通过向寄存器指定特定的值来实现。一会你看了我们要构建的引导程序的解释之后,就不会再有疑问了。
一个很重要的事实是,相同的指令可以出于不同的目的而调用。具体要用的目的可以根据函数号来区分。这个函数号会存放在ah寄存器中。比如,13号中断即可以用来在屏幕上显示一串字符,也可以取得光标的位置。如果我们设置ah寄存器的值为3,那么就会去取得光标的位置。要想在屏幕上显示些什么,就得将ah寄存器的值设置为13h。

2 接下来做什么?
这一次我们的源码包括两个汇编程序和一个C程序。第一个汇编代码是引导程序。在启动程序中,我们写一些代码,让它将第二个扇区的内容复制到内存段0x500处(地址为0x5000)。在这里就需要使用13号中断。然后引导程序就将控制权交给以0x500为基址,偏移量为0的地方。第二个汇编文件内的代码是使用10号中断在屏幕上打印一行消息。C程序的作用就是将第一个汇编文件生成的可执行文件拷贝到软盘的第一个扇区中,将第二个汇编文件生成的可执行文件拷贝到第二个扇区当中。

3 引导程序
使用13号中断,引导程序将第二个扇区的内容拷贝到内存地址0x5000处(段地址0x500)。下面给出的代码就是完成这个工作,将其保存为bsect.s。

  1. LOC1=0x500

  2. entry start
  3. start:
  4.         mov ax,#LOC1
  5.         mov es,ax
  6.         mov bx,#0  

  7.         mov dl,#0  
  8.         mov dh,#0  
  9.         mov ch,#0  
  10.         mov cl,#2  
  11.         mov al,#1  

  12.         mov ah,#2  

  13.         int 0x13

  14.         jmpi 0,#LOC1
复制代码

第一行和宏很像。再下面的两行,你现在应该很熟悉了。然后我们将值0x500载入到es寄存器中。从第二个扇区中考出数据就要拷贝到这里(第一个扇区是启动扇区)。然后我们将段内依稀量设为0.
下面我们将驱动器号载入dl寄存器,将柱头号载入dh寄存器,将磁道号载入ch寄存器,扇区号载入cl寄存器,并且将要拷贝的扇区数量载入到registeral。所以我们将会把第二个扇区的第0个磁道,第0个驱动载入到段0x500上。上面所有一切都是基于1.44M软盘的。
将2载入ah寄存器是选择功能号。这个选择是13号中断提供的。我们选的这上函数代表从软盘拷贝数据。
现在我们就要调用13号中断,然后跳到段0x500处。

4 第二部分
下面就是每二部分的代码:

  1. entry start
  2. start:
  3.         mov     ah,#0x03                 
  4.         xor     bh,bh
  5.         int     0x10

  6.         mov     cx,#26                  
  7.         mov     bx,#0x0007               
  8.         mov     bp,#mymsg
  9.         mov     ax,#0x1301               
  10.         int     0x10

  11. loop1:  jmp     loop1

  12. mymsg:
  13.         .byte  13,10
  14.         .ascii "Handling BIOS interrupts"
复制代码

    这段代码将会被载入到0x500处并且被执行。这里的代码调用了10号中断取得光标位置,然后打印一行消息。
    开始的三行代码用来取得光标的当前位置(从第三行开始)。在这里13号中断第三个函数被调用。然后我们清空bh寄存器,把字符串载入到ch寄存器中。在bx寄存器中我们载入的是页面号和显示时的属性。我们打算用黑底白字。消息使用CR和LF的组合作为换行符,它们的值分别为13和10。字符串由24个字符构成。然后我们选择函数在光标处打印它们。然后调用中断。在最后是循环。

5 C程序
下面是C程序的代码,将其保存为write.c。

  1. #include <sys/types.h> /* unistd.h needs this */
  2. #include <unistd.h>    /* contains read/write */
  3. #include <fcntl.h>

  4. int main()
  5. {
  6. char boot_buf[512];
  7.                 int floppy_desc, file_desc;

  8.                 file_desc = open("./bsect", O_RDONLY);

  9.                 read(file_desc, boot_buf, 510);
  10.                 close(file_desc);

  11.                 boot_buf[510] = 0x55;
  12.                 boot_buf[511] = 0xaa;

  13.                 floppy_desc = open("/dev/fd0", O_RDWR);

  14.                 lseek(floppy_desc, 0, SEEK_SET);
  15.                 write(floppy_desc, boot_buf, 512);

  16.                 file_desc = open("./sect2", O_RDONLY);
  17.                 read(file_desc, boot_buf, 512);
  18.                 close(file_desc);

  19.                 lseek(floppy_desc, 512, SEEK_SET);
  20.                 write(floppy_desc, boot_buf, 512);

  21.                 close(floppy_desc);
  22. }
复制代码

    在这篇文章的第一个部分,我描述了制作引导盘的过程。这里有一点小小的不同。我们首先拷贝由bsect.s生成的可执行文件bsect到启动扇区,然后再拷贝由sect2.s生成的可执行文件sect2到软盘的第二个扇区。这样的改变使用软盘同样可以启动系统。

6 编译
在原文中,这一节名为download。它提供了各个源文件的下载地址。但是在我看的pdf文件中,这个链接不能显示,所以也就略过不写。在这节中唯一前面没有提到的东西就是Makefile。下面我把它粘也来:
  1. all : bsect sect2 write

  2. bsect : bsect.o  
  3. ld86 -d bsect.o -o bsect

  4. sect2 : sect2.o  
  5. ld86 -d sect2.o -o sect2

  6. bsect.o : bsect.s
  7. as86 bsect.s -o bsect.o

  8. sect2.o : sect2.s
  9. as86 sect2.s -o sect2.o

  10. write : write.c
  11. cc write.c -o write
  12. clean :
  13. rm bsect.o sect2.o bsect sect2 write
复制代码

将上面的代码保存为makefile。最好在检查一下,看看有没有tab被替换成空格的情况出现,这种错误可是真的挺烦人的哦,呵呵。

将几个源文件和这个Makefile放在同一个目录下面,然后在提示符后直接输入:
Make
就可以了。当然也可以在提示符下分别的编译它们:
  1. as86 bsect.s –o bsect.o
  2. ld86 –d bsect.o –o bsect
复制代码

sect2.s文件也是这样编译。编译write.c文件,将软盘放进去,执行:
  1. cc write.c –o write
  2. ./write
复制代码


7 接下来会是什么?
    系统启动以后你会看到字符串被显示出来。这样我们就成功的使用了BIOS中断。在这个系列的下一个部分我希望可以写我们如何将CPU切换到保护模式下。再见!
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-11-12 10:19:47 | 显示全部楼层
英文水平有限,译文很糟。
可以阅读原文请直接看看英文版的吧,呵呵。
希望兄弟们喜欢!
还有第三部分,暂时还没弄完~~~
回复 支持 反对

使用道具 举报

发表于 2007-11-12 13:05:23 | 显示全部楼层
不错,兄弟加油!
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-11-12 15:01:49 | 显示全部楼层
PART III
在第一部分和第二部分我们讲述了用Linux的一些工具构建一个小的引导程序,并调用BIOS系统的过程。下面我们的玩具系统将会更加接近于历史上的Linux Kernel,所以我们要忙的切换到保护模式!这部分的内容将告诉你怎么去做。

1 什么是保护模式?
  80386+提供了很多8086所不支持的新特性,比如像内存保护,虚拟内存,多任务,及大于640K的内存支持……但是它仍然与8086系统相兼容。386拥有8086和286的所有特性,当然有很大的提升。像早期的处理器一样,它有实时模式。像286一样,386也可以在保护模式下运行。但是,386的保护模式已经非常的不同。与286相比,386的保护模式给程序员提供了更好的保护和更多的内存。保护模式的目标不是要保护你的程序,而是保护所有的东西(包括操作系统)不受你程序的影响。

1.1        保护模式 VS 实时模式
简单的保护模式和实时模式看起来没有多少不一样的地方。它们都使用分段的内存,使用驱动程序和中断来控制设备。但是确实有证据表明它们是不同的两个模式。
在实时模式下,我们可以把内存视为由至少16字节组成的64k的段。分段是由段寄存器内置的机制控制的。这些段寄存器(cs,ds,ss,……)的内容都是一些物理地址,这些地址都是由CPU设置在地址总线上的。物理地址可以由段寄存器的值乘以16再加上16位的偏移量得出。就是这个16位的偏移量限制了我们,使我们只有64k大小的段。
fig 1: Real Mode Addressing


在保护模式下,分段是通过一组称为描述符表的的表状结构定义的。段寄存器保存着这些表的入口指针。有两种不同的表来共同定义内存的分段:全局描述符表(GDT)和局部描述符表(LDT)。
GDT保留着所有应用程序可以访问的基础描述符。在实时模式下,一个段是64k大小接着一个16位的偏移量,而在保护模式下,我们就有一个随时可用的有4Gb大小的段。LDT则保存着由任务或程序指定的段的信息。系统可以建立一个GDT做为其系统描述符,然后再为每个任务建立一个LDT,每个描述符长8位。它的格式请参考图3.每次加载段寄存器,基地址就从适当的表项中取出。表项的内容存储在程序可见的一个寄存器中,它的名字叫做影子寄存器,这样在将来再使用相同的寄存器的时候就可以从这个寄存器中取出信息而不必每一次都向表发出申请。物理地址是由向影子寄存器中的基址加一个16位或是32位的偏移量得到的。保护模式和实时模式的不同,通过图1和图2可以很清楚的表现出来。
fig 2: Protected Mode Addressing

fig 3 : Segment Descriptor Format

还有一个称为中断描述符(IDT)的表我们没有描述。IDT表包含着中断描述符。它们用于告拆CPU在哪能找到中断处理句柄。每个中断包含一个入口项,就像在实时模式下一样,但是表项的格式却完全不同。我们切换到保护模式的代码中不使用IDT,所以它的细节我们将不再讲述。
2        进入保护模式
386有四个32位的控制寄存器,分别为CR0,CR1,CR2和CR3。CR1是为将来的处理器预留的,在386中没有定义。CR0保存的位来标识是否支持分页,是否是保护模式和是否支持浮点协处理器操作。CR2和CR3被分页机制使用。我们只关心CR0的第0位和保护模式开关PE位。当PE=1,处理器会使用前面说过的分段机制进入保护模式。如果PE=0,处理器会进入实时模式。386还有像GDTR,LDTR和IDTR这样的分页表基址寄存器。这些寄存器记录描述符表的地址。GDTR指向GDT表。48位的GDTR使用32位的线性地址定义GDT的基址,用16位的限定符界定GDT表的范围。
切换到保护模式实际上是暗示我们要设置PE位的值。但是在这之前我们还一些别的事要做。程序必须得初始化系统段和控制寄存器。在设置PE位为1之后,我们马上要执行一个跳转指令来清空所有指令的执行管道,否则它们可能在实时模式下被错误的取出执行。this jump is typically to the next instruction。切换到保护模式的次序如下:
1.        建立GDT。
2.        设置CR0寄存器的PE位为1,打开保护模式。
3.        跳转,清空预取队列。
我将会给出代码来完成这个切换。

3        我需要的东西
        *一张空的软盘
        *NASM汇编器。
   点击这里下载代码。

  1. org 0x07c00             ; Start address 0000:7c00
  2. jmp short begin_boot   ; Jump to start of boot routine & skip other data

  3. bootmesg db "Our OS boot sector loading ......"
  4. pm_mesg  db "Switching to protected mode ...."

  5. dw 512                   ; Bytes per sector
  6. db 1                    ; Sectors per cluster
  7. dw 1                    ; Number of reserved sectors
  8. db 2                    ; Number of FATs
  9. dw 0x00e0            ; Number of dirs in root
  10. dw 0x0b40             ; Number of sectors in volume
  11. db 0x0f0                       ; Media descriptor
  12. dw 9                    ; Number of sectors per FAT
  13. dw 18                    ; Number of sectors per track
  14. dw 2                    ; Number of read/write sectors
  15. dw 0                    ; Number of hidden sectors

  16. print_mesg :
  17.    mov ah,0x13            ; Fn 13h of int 10h writes a whole string on screen
  18.    mov al,0x00            ; bit 0 determines cursor pos,0->point to start after                                       ; function call,1->point to last position written
  19.    mov bx,0x0007            ; bh -> screen page ie 0,bl = 07 ie white on black
  20.    mov cx,0x20            ; Length of string here 32
  21.    mov dx,0x0000            ; dh->start cursor row,dl->start cursor column
  22.    int 0x10            ; call bios interrupt 10h
  23.    ret                    ; Return to calling routine

  24. get_key :
  25.    mov ah,0x00          
  26.    int 0x16              ; Get_key Fn 00h of 16h,read next character
  27.    ret

  28. clrscr :
  29.    mov ax,0x0600      ; Fn 06 of int 10h,scroll window up,if al = 0 clrscr
  30.    mov cx,0x0000      ; Clear window from 0,0
  31.    mov dx,0x174f      ; to 23,79
  32.    mov bh,0           ; fill with colour 0
  33.    int 0x10           ; call bios interrupt 10h
  34.    ret

  35. begin_boot :
  36.    call clrscr             ; Clear the screen first
  37.    mov bp,bootmesg     ; Set the string ptr to message location
  38.    call print_mesg       ; Print the message
  39.    call get_key            ; Wait till a key is pressed
  40. bits 16
  41.    call clrscr            ; Clear the screen
  42.    mov ax,0xb800            ; Load gs to point to video memory
  43.    mov gs,ax            ; We intend to display a brown A in real mode
  44.    mov word [gs:0],0x641   ; display
  45.    call get_key          ; Get_key again,ie display till key is pressed       
  46.    mov bp,pm_mesg            ; Set string pointer          
  47.    call print_mesg            ; Call print_mesg subroutine   
  48.    call get_key          ; Wait till key is pressed
  49.    call clrscr             ; Clear the screen
  50.    cli                    ; Clear or disable interrupts
  51.    lgdt[gdtr]            ; Load GDT       
  52.    mov eax,cr0            ; The lsb of cr0 is the protected mode bit
  53.    or al,0x01            ; Set protected mode bit
  54.    mov cr0,eax            ; Mov modified word to the control register
  55.    jmp codesel:go_pm

  56. bits 32
  57. go_pm :
  58.    mov ax,datasel   
  59.    mov ds,ax               ; Initialise ds & es to data segment
  60.    mov es,ax       
  61.    mov ax,videosel               ; Initialise gs to video memory
  62.    mov gs,ax       
  63.    mov word [gs:0],0x741 ; Display white A in protected mode
  64. spin : jmp spin             ; Loop

  65. bits 16
  66. gdtr :
  67.    dw gdt_end-gdt-1      ; Length of the gdt
  68.    dd gdt                       ; physical address of gdt
  69. gdt
  70. nullsel equ $-gdt           ; $->current location,so nullsel = 0h
  71. gdt0                        ; Null descriptor,as per convention gdt0 is 0
  72.    dd 0                       ; Each gdt entry is 8 bytes, so at 08h it is CS
  73.    dd 0                      ; In all the segment descriptor is 64 bits
  74. codesel equ $-gdt               ; This is 8h,ie 2nd descriptor in gdt
  75. code_gdt                       ; Code descriptor 4Gb flat segment at 0000:0000h
  76.    dw 0x0ffff               ; Limit 4Gb  bits 0-15 of segment descriptor
  77.    dw 0x0000               ; Base 0h bits 16-31 of segment descriptor (sd)
  78.    db 0x00                  ; Base addr of seg 16-23 of 32bit addr,32-39 of sd       
  79.    db 0x09a               ; P,DPL(2),S,TYPE(3),A->Present bit 1,Descriptor                                       ; privilege level 0-3,Segment descriptor 1 ie code                                              ; or data seg descriptor,Type of seg,Accessed bit
  80.    db 0x0cf               ; Upper 4 bits G,D,0,AVL ->1 segment len is page                                                     ; granular, 1 default operation size is 32bit seg                                              ; AVL : Available field for user or OS
  81.                               ; Lower nibble bits 16-19 of segment limit
  82.    db 0x00               ; Base addr of seg 24-31 of 32bit addr,56-63 of sd
  83. datasel equ $-gdt               ; ie 10h, beginning of next 8 bytes for data sd
  84. data_gdt                       ; Data descriptor 4Gb flat seg at 0000:0000h
  85.    dw 0x0ffff               ; Limit 4Gb
  86.    dw 0x0000               ; Base 0000:0000h
  87.    db 0x00               ; Descriptor format same as above
  88.    db 0x092
  89.    db 0x0cf
  90.    db 0x00
  91. videosel equ $-gdt               ; ie 18h,next gdt entry
  92.    dw 3999               ; Limit 80*25*2-1
  93.    dw 0x8000               ; Base 0xb8000
  94.    db 0x0b
  95.    db 0x92               ; present,ring 0,data,expand-up,writable
  96.    db 0x00               ; byte granularity 16 bit
  97.    db 0x00
  98. gdt_end

  99. times 510-($-$$)  db 0  ; Fill bytes from present loc to 510 with 0s
  100.               dw 0x0aa55  ; Write aa55 in bytes 511,512 to indicate that                                          ; it is a bootable sector.
复制代码


将这些代码保存为abc.asm。使用命令
  1. nasm abc.asm
复制代码

来编译它,会生成一个名为abc的文件。然后插入软盘,输入下面的命令:
  1. dd if=abc of=/dev/fd0
复制代码

这个命令将文件abc写入到软盘的第一个扇区中。然后重启系统,你应该可以在屏幕上看到下面的消息。
•        Our os booting........................
•        A (Brown colour)
•        Switching to protected mode....
•        A (White colour)

4        代码做了一切!
我会先给出代码来完成切换,再给出详细的解释。
在前面的文章提到过的(part I),在BIOS中选择启动设备并且将其第一个扇区放到内存0x7c00中,这样我们就可以在0x7c00处启动代码。指导就是这么暗示我们的。
使用的函数
print_mesg: 这个例程使用了BIOS的10号中断的第13个子程序来向屏幕上打印字符串。通过向各种寄存器替换数值来设置属性。10号中断用来进行各种字符操作。我们在ah寄存器存入一个子程号13h来表示我们想要打印一行消息。al寄存器的第0个位探明了光标的下一个位置;如果它是0的话,我们返回下一行的开始,如果它是1的话,光标会马上跟在最后一个字符的后面。
显存被分割成几个称为显存页面的部分。每次只有一个页面能被显示(更多显存的细节请参考PART I)。bh寄存器保存了页面数,bl寄存器指明被打印字符的颜色。cx寄存器要打印字符的个数,dx寄存器刚指明光标的位置。一旦所有的属性都设置好了,我们就调用10号中断。

get_key: 我们使用了16号中断的第0个函数来从屏幕取得下一个字符。ah寄存器保存了子程号。

clrsrc: 这个函数使用了10号中断的另一些子程,比如使用06h在打印字符之前清空屏幕。为了这个操作,我们将al寄存器初始化为0,cx和dx寄存器指定要清除的屏幕大小,在这里我们清除整个屏幕。bh寄存器指定屏幕背景色,在这里设置为黑色。
一切从哪里开始!!
汇编代码的第一行是跳转到开始启动的代码。我们要在实时模式下打印一个棕色的’A’ ,建立GDT表,然后切换到保护模式下打印一个白色的’A’。两种模式各自使用它们自己的寻址方法。

实时模式下:
我们使用gs段寄存器指向显存入口。我们使用CGA适配器(默认基址是0xb8000)。但是在代码中我们少了一个0。因为在实时模式下,段单元会额外加上一个0。这种情况很方便,因为8086通常操作20位的地址。这种方法也被带到了80386的实时模式。字符A的ascii码是0x41,0x06表示我们想到一个棕色的字符显示。它会一直在那里显示,直到我们按下一个键。然后会打印一行消息说我们即将要进入到保护模式。所以我们要指向bp(基础指针寄存器指向即将被打印的消息)。

切换到保护模式下:
处于保护模式下,我们不需要被任何的中断所打扰,是吧?所以我们将中断禁止掉。这就是cli指令的作用。我们会在后面再次打开中断。咱们从建立GDT表开始吧,在这个实验中我们初始化了4个描述符来进入保护模式。这些描述符初始化我们的代码段,数据段,堆栈段和显存段,这样才能访问显存。还有一个没用的描述符也被初始化了,虽然它从来都不会被使用,当然,除非你想要个三键热启动(triple fault);这是一个空的描述符。让我们来看一下段描述符的各个域吧。
        第一个字保存着段的长度,为简便起见,我们设置其为FFFF(4G)。对于显存,我们预设其为3999(80列*25行*2字节 - 1)。
        代码段和数据段的基址设置为0x0000。显存段基址设置为0xb8000。
GDT表基地址需要被载入到GDTR系统寄存器中。gdtr段将GDT表的大小载入到第一个字中,将GDT表的基址载入到它的下一个双字中。然后lgdt指令将gdt段载入到GDTR寄存器中。现在我们已经准备好切换到保护模式下了。我们由设置CR0寄存器有意义的位为1开始(比如PE位),现在我们还没有进入完全的保护模式!
按《INTEL 80386 PROGRAMMER'S REFERENCE MANUAL》的10.3节的描述:设置好PE位之后,要立即使用一个跳转指令清空CPU的预取指令队列。80386会在指令被执行之前被取出并且解码定位;无论如何,切换到保护模式之后,预先取出的指令信息(这些信息在实时模式下有效)都不再有效。JMP会迫使处理器丢弃不正确的信息。
现在我们在保护模式下了。想要检验一下么?咱们用白色打印一个字符’A’吧。要做这些需要我们使用数据段选择子(datasel)初始化数据段和扩展段,使用显存段选择子(videosel)初始化gs寄存器。
要想打印一个白色的’A’,要把由ascii码值和属性组成的字移动到[gs:0000]这样的地方,比如b8000:0000。持续的循环会一直打印这些文字直到计算机被重新启动。
times指令用来将扇区内未使用的字节填充0。为了表明这是一个启动盘,在511和512处写入AA55。就是这样。

PS:
    我不知道怎么在合适的地方插入图片,所以这部分的三个插图我以附件的形式发出。还是那句话,如果能看原文,就看原文。我的翻译实在太差,可能也只有自己能看明白:flash:

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
回复 支持 反对

使用道具 举报

发表于 2007-11-13 10:23:06 | 显示全部楼层
注:看到这里,我真庆幸当时不顾别的意见给自己的机器安装了软驱.我知道要做上面的实验即使没有软驱也一样可以,但是会浪费一些时间,呵呵.看来硬件是不是过时,是不是太过于古老,要看你想要它来做什么.

相比较起来,用软驱要更浪费时间,这种东西最好放在虚拟机里来运行,否则改一下就得重启才更浪费时间
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-11-14 16:02:39 | 显示全部楼层
Post by small_bee;1781630
注:看到这里,我真庆幸当时不顾别的意见给自己的机器安装了软驱.我知道要做上面的实验即使没有软驱也一样可以,但是会浪费一些时间,呵呵.看来硬件是不是过时,是不是太过于古老,要看你想要它来做什么.

相比较起来,用软驱要更浪费时间,这种东西最好放在虚拟机里来运行,否则改一下就得重启才更浪费时间
其实也没有想去改它的代码。
只是觉得如果这篇文章好好看一下,确认它的代码是可以运行的,会对Linux启动代码的阅读有帮助。
回复 支持 反对

使用道具 举报

发表于 2007-11-16 11:00:45 | 显示全部楼层
收藏,有时间再看
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表