分类:
2010-09-07 21:54:26
本系列旨在展示和教授如何从最底层开始开发一个操作系统。
欢迎光临!本章教程相信您已期待很久。本章我们将涵盖许多话题,如:
准备好了吗?
当你按下电源按钮,实际上发生了什么?当这个按钮被按下,连接到这个按钮的线向主板发送一个电子信号。主板仅仅重定向这个信号到电源(power supply)(PSU)。
这个信号包含了单个位的数据。如果它是0,那么,当然,没有电源(所以电脑是关闭的,或者主板是失效的(the motherboard is dead))。如果它是1(意味着一个有效的信号),这意味着电源已经正在供电。
为了更好地理解它,记住计算机中的二进制逻辑的基础。8个“位”仅仅代表8根电流能够通过的“线”。0表示当前没有电流,而1表示当前电线中有电流。这个再加上逻辑门,就是数字逻辑电子学的基础,计算机就是在其基础上建立起来的。
当电源接收到活动信号,它就开始给系统其余部分供电。当给所有设备提供了正确数额的电压,电源将能够继续补充那些没有重大问题的电源。
接着电源发送一个信号,一个被称为“power_good”的信号,给主板中的基本输入输出系统(BIOS)。
当BIOS接收到”power_good”信号,BIOS就会开始一个成为自检(Power On Self Test)的过程。自检会测试提供的电压以确保足够,会测试安装的设备(例如键盘,鼠标,USB,串口等等),还有保证内存可用(通过测试内存是否损坏)。
接着自检会将控制权交给BIOS。自检将BIOS加载到内存的结尾(可能是0xFFFFF0)并且将一个跳转指令放在内存的第一个字节。
处理器的指令指针(CS:IP)被设为0,接着处理器获得控制权。
这意味着什么?处理器开始执行地址0×0的指令。在这里,是被自检所放的跳转指令。这个跳转指令会跳到0xFFFFF0(或者其他BIOS被加载的位置),接着处理器开始执行BIOS。
BIOS获得控制权…
基本输入输出系统(BIOS)会做这么几件事。它创建一个中断向量表(IVT),并提供一些基本的中断服务。然后BIOS会做一些测试以确保没有硬件问题。BIOS也提供一个设置工具。
接着BIOS需要找到一个操作系统。基于你在BIOS中所设的引导顺序,BIOS会执行中断(INT)0×19来试着找到一个可引导的设备。
如果没有发现引导设备(INT 0×19返回),BIOS接着试引导顺序中所列的下一个设备。如果没有设备了,它会打印一条错误就像“没有发现操作系统”这样的然后停止系统。
一个中断就是一个可以通过许多不同程序运行的子程序。这些中断被存放在地址0×0开头的一个成为中断向量表的表里面。例如,一个常用的中断是DOS中使用INT 0×21。
备注:这里没有DOS!“仅有”的可用中断是由BIOS提供的,除此之外没有别的了!使用其他的中断将会引起系统执行一个不存在的例程,导致你的程序崩溃!
备注:如果你转换处理器模式,IVT将不再有效。这意味这绝对“没有”中断——既无软件中断也无硬件中断可用,甚至没有BIOS中断。对于一个32位的操作系统,我们不得不这样做。虽然现在还没有!
INT 0×19 – SYSTEM:BOOTSTRAP LOADER
通过热启动重启系统而并不清除内存以及恢复中断向量表
这个中断是被BIOS执行的。它读取第一个硬盘的第一个扇区(1扇区,0扇面,0磁道)。
一个扇区仅仅代表一组512字节。因此,扇区1代表一个磁盘的第一个512字节。
一个“扇面”(或者面)代表磁盘的一个侧边,扇面0是前侧,扇面1是后侧。大多数磁盘只有1个侧边,因此只有一个扇面(“扇面0”)。
为了理解磁道,我们看下面一张图:
在这张图中,这个磁盘可以代表一个硬盘或者一个软盘。这里我们看到的是扇面1(前侧),并且扇区代表512字节。一个磁道是扇区的集合。
备注:记住一个扇区是512字节,软盘上每个磁道有18个扇区。这在我们加载文件时是非常重要的。
如果这个磁盘是可引导的,引导扇区将会被加载到0x7C00,然后INT 0×19会跳到它,从而使bootloader获得控制权。
备注:记住bootloader被加载到0x7C00,这是非常重要的!
备注:在某些系统上,你也可以通过在地址0×0040:0072存储0×1234值,然后跳转到0xFFFF:0来执行一个热启动。要想冷重启,则存储0×0。
现在,我们的1337 bootloader已在控制之中。
我们已经谈到过很多次bootloader了。让我们把重要的部分集中起来!
目前为止,Bootloader…
你可以想象的到,在512字节中我们并不能做太多,那我们该怎么办呢?
在汇编语言中,我们可以非常容易地越过512字节的位置。因此,代码可能看起来很好,但仅仅只有部分会出现在内存中。例如,考虑这种情况:
1 | mov ax, 4ch |
2 | inc bx ; 512 byte |
3 | mov [var], bx ; 514 byte |
在汇编语言中,程序从文件的顶部向下执行。然而,记住,当将文件载入内存时,我们加载的是扇区。每一个扇区是512字节,所以它只会将文件的512字节复制到内存中去。如果上面的代码被执行,而仅仅第一个扇区被加载到内存,它只会复制到512字节的地方(inc bx指令)。因此,最后的mov指令仍然在磁盘上,它不在内存中!
那么在inc bx指令之后处理器会做什么呢?它会继续执行到514字节的地方。因为它不再内存中,它会执行超过文件的结尾!结尾意味着什么?崩溃!
然而,加载第二个扇区(或者更多)到一个给定的地址并执行也是可能的。这样文件剩余的部分就会在内存中,然后所有的东西都会正常工作了。
这种方法可以工作,但是却很难实现。最通常的方法是保持bootloader的大小在512字节以内,搜索,加载并执行一个二级bootloader。我们后面会详细看看这个方法。
硬件异常很像软件异常,但处理器会执行他们,而非软件。
有些时候我们需要停止所有的异常,以防止他们发生。例如,在切换计算机模式时,整个中断向量表将不再有效,因此,任何硬件或软件中断,都会导致你的系统崩溃。后面会详细讲到。
你可以使用STI和CLI指令来启用和禁用所有的中断。大多数系统禁止在应用程序中使用这些指令,因为它可能会导致大问题(虽然系统能模拟他们)。
1 | cli ; clear interrupts |
2 |
3 | ; do something... |
4 |
5 | sti ; enable interrupts--we're in the clear! |
如果处理器在执行过程中发现了一个问题(如一个无效的指令,除以0等等),它就会执行一个二级故障异常处理程序(双重故障),也就是中断0×8。
我们将在不久之后看到一个双重故障。如果处理器在一个双重故障之后仍不能继续,则它会执行一个三重故障。
我们在以前看到过这个术语,不是吗?一个CPU的“三重故障”仅仅意味着系统硬重启。
在开始的几级,如bootloader,无论你的代码的任何地方有bug,系统都会引起三重故障。这表示你的代码中有个问题。
我们等待的时刻终于到了!:)
让我们再一次看看我们的列表:
打开任何一个普通的文本编辑器(我使用的Visual Studio 2005),记事本已经足够了。
下面是bootloader(Boot1.asm)…
01 | ;********************************************* |
02 | ; Boot1.asm |
03 | ; - A Simple Bootloader |
04 | ; |
05 | ; Operating Systems Development Tutorial |
06 | ;********************************************* |
07 |
08 | org 0x7c00 ; We are loaded by BIOS at 0x7C00 |
09 |
10 | bits 16 ; We are still in 16 bit Real Mode |
11 |
12 | Start: |
13 |
14 | cli ; Clear all Interrupts |
15 | hlt ; halt the system |
16 |
17 | times 510 - ($-$$) db 0 ; We have to be 512 bytes. Clear the rest of the bytes with 0 |
18 |
19 | dw 0xAA55 ; Boot Signiture |
这些本不会带给我们太多惊喜。让我们一行一行分析:
1 | org 0x7c00 ; We are loaded by BIOS at 0x7C00 |
记住:BIOS将我们加载到0x7C00。上面的代码告诉NASM以保证所有的地址都是相对于0x7C00。这意味着,第一条指令将会在0x7C00处。
1 | bits 16 ; We are still in 16 bit Real Mode |
还记得第二章的内容吗?在那章中,我解释过x86家族是如何向后兼容那些老的DOS系统的。因为老的DOS系统都是16位的,因此所有x86兼容的计算机启动后进入16位模式。这意味着:
我们需要将计算机转换为32位模式,我们将后面做这件事。
1 | times 510 - ($-$$) db 0 ; We have to be 512 bytes. Clear the rest of the bytes with 0 |
我希望有更多关于这个的文档。在NASM中,美元符号($)代表当前行的地址,$$代表第一条指令的地址(应该是0x7C00)。因此,$-$$返回的是当前行到开始处的字节数(在本例中,就是程序的大小)。
1 | dw 0xAA55 ; Boot Signiture |
这个需要一些解释。
记住,BIOS INT 0×19搜索可引导的磁盘。它怎么知道磁盘是可引导的呢?答案是引导标签。如果511字节是0xAA,并且512字节是0×55,那么INT 0×19将会加载并执行bootloader。
因为引导标签必须在bootloader的最后两个字节,所以我们使用times关键字来计算可以填充到第510字节的可变大小,而不是第512字节。
NASM是一个命令行汇编器,因此必须通过命令行或者批处理脚本来执行。为了汇编Boot1.asm需要执行下面的命令:
1 | nasm -f bin Boot1.asm -o Boot1.bin |
-f选项用来告知NASM所生成的输出文件的类型。在本例中是二进制程序。
-o选项用来给出一个生成的文件的文件名。在本例中是Boot1.bin。
汇编之后,你会得到一个精确的512字节名叫“Boot1.bin”的文件。
备注:由于某些原因,windows资源管理器显示的大小限制为1kb。查看文件的属性,它会告知为512字节。
我们将会使用VFD来创建一个虚拟软盘镜像用于将我们的操作系统复制进去。现在解释如何使用它。
确保media类型为标准的3.5英寸1.44MB软盘,磁盘类型为RAM。同时,保证写保护为禁用状态。点击“创建”按钮。
打开我的电脑,(在你的电脑上)你将会看到一个新的软盘驱动器。
右键点击驱动器选择属性来格式化磁盘。在VFD选项卡下会有一个格式化选项。
很好…现在我们的bootloader已经准备好了,我们如何把它复制到磁盘呢?你或许知道,Windows不允许我们直接将它复制到磁盘的第一个扇区。因此,我们需要使用一个命令来实现它。
在第一章我们已经看了一个这样的命令:debug。如果你已经决定使用这个命令,你可以略过关于partcopy的这一节。
PartCopy是一个命令行程序。它的使用形式如下:
1 | partcopy file first_byte last_byte drive |
PartCopy的功能不仅限于复制文件。它可以向或者从扇区复制特定的数量的字节。使用它的格式(上面显示的)是一种安全的方法。
因为你已经模拟了一个软盘驱动器,所以你可以通过一个字母名字来引用这个驱动器(就像A:)。
下面的命令可以复制我们的bootloader:
1 | partcopy Boot1.bin 0 200 -f0 |
f0代表软盘0。你可以在f0和f1之间改变等等,根据你的软盘在哪个驱动器来决定。Boot1.bin是我们要复制的文件。它复制文件的第一个字节(0×0)到最后的一个字节(0×200,就是十进制的512)。注意partcopy仅仅接收16进制的数。
警告:记住如果你使用这个程序不小心可能会导致磁盘的永久性损坏。上面的命令行命令只适用于软盘,不要试图将其用于硬盘上。
Bochs是一个32位的PC模拟器。我们将使用Bochs来调试和测试。
Bochs使用一个配置文件来描述所模拟的硬件。例如,下面是我所使用的配置文件:
01 | # ROM and VGA BIOS images --------------------------------------------- |
02 |
03 | romimage: file=BIOS-bochs-latest, address=0xf0000 |
04 | vgaromimage: VGABIOS-lgpl-latest |
05 |
06 | # boot from floppy using our disk image ------------------------------- |
07 |
08 | floppya: 1_44=a:, status=inserted # Boot from drive A |
09 |
10 | # logging and reporting ----------------------------------------------- |
11 |
12 | log : OSDev. log # All errors and info logs will output to OSDev. log |
13 | error: action=report |
14 | info: action=report |
配置文件使用#号来表示注释。它会试着从A盘的任何软盘镜像(比如说我们用VFD创建的那个)启动。ROM BIOS和VGA BIOS镜像来自Bochs,所以你不需要担心它。
配置文件中的很多行都非常简单,然而有一行我们需要在这里看看:
1 | romimage: file=BIOS-bochs-latest, address=0xf0000 |
这一行告诉Bochs将BIOS放在内存(虚拟RAM)的什么地方。还记得BIOS的大小有可能不同吗?同样还记得BIOS必须在内存中的第一个兆字节的结尾处(0xFFFFF)结束吗?
因此,你需要改变这一行来重定位Bios。可以通过获取Bios镜像的大小来实现它(在你的Bochs目录中它应该被命名为BIOS-bochs-latest)。获取大小的字节数。
之后,简单的减法0xFFFFF – bochs文件的大小(字节)。这就是新的Bios地址,然后在那行上更新这个地址就可以将Bios移到它新的地址。
你可以用或者不用做这一步。如果你在Bochs测试中获得一个关于Bios必须在0XFFFFF结尾的错误,这是你需要完成这一步它才会工作。
…那么你刚刚经历了一个三重故障(Triple Fault),回到代码中试着找找问题在哪里。如果你需要任何帮助,尽管联系我。
恭喜!那是我们的cli和hlt指令暂停了系统,所以我们知道我们的bootloader被执行了。
将我们已经做的与前面章节我们看到的创建过程相比较,你习惯它之后,你会觉得非常简单。
从现在开始,我不会再描述创建过程中的步骤的具体细节。
未完待续!