上一节中说到BIOS会将MBR中的主引导程序(512字节)加载到内存的0x7c00处,其中这512字节的主引导程序是软件程序,是操作系统的一部分,因此也是由操作系统开发者来编写的,BIOS将其加载到内存后,会自动跳到0x7c00处去执行。接下来我们自己实现一个“主引导程序”,功能很简单,就是让它打印一串字符串到屏幕上(真正的主引导程序是加载操作系统内核用的),注意,这段程序现在是独立运行在裸机上的,我们用汇编语言来实现它。
这个过程我们需要调用中断向量表中的“打印函数”,调用的动作是通过int 0x10来实现的。
编写程序之前,我们先来介绍几个要用到的指令:
mov :赋值操作,将右操作数赋值给左操作数
例:mov ax, 0 ;将0赋值给ax寄存器
int :触发中断
例:int 0x10 ; 触发0x10中断,对屏幕进行操作
hlt :停止,CPU进入暂停状态,不进行任何操作
例:hlt ;使程序进入睡眠状态
汇编中的地址访问形式:段地址:段内偏移地址
例:mov byte[0xb800:0x01] ; 0xb800:0x01 -> 0xb800 + 0x01
标签:
用于标识后续指令的地址,(与C语言中的标签等价)
$ 与 $$
$表示当前指令行地址,$$表示当前汇编段起始地址
中断调用与函数调用的对应关系如下所示:
实现真正的打印之前,要向bx寄存器中写入0x0f,向ah寄存器中写入0x0e,这些都是打印函数规定好的。而al寄存器中需要存入要打印的字符,寄存器中的内容准备好了之后使用int 0x10来调用打印函数,而后,就可以将字符打印到屏幕上。
下面直接给出用汇编语言写的“主引导程序“。
org 0x7c00 start: mov ax, cs mov ss, ax mov ds, ax mov es, ax mov si, msg print: mov al, [si] add si, 1 cmp al, 0x00 je last mov ah, 0x0e mov bx, 0x0f int 0x10 jmp print last: hlt jmp last msg: db 0x0a, 0x0a db "Hello, DTOS!" db 0x0a, 0x0a times 510-($-$$) db 0x00 db 0x55, 0xaa
主引导程序实际待的起始地址是0x7c00,因此第一行的org 0x7c00告诉编译器,这段程序的加载地址是0x7c00,这样编译器就可以对地址进行正确的处理。
然后将ss,ds,ed寄存器分别清零,msg标签处在内存中定义了一些数据,这些数据是实实在在在内存中和磁盘文件中占用空间的。msg就代表了这片数据所占用空间的起始地址。mov si, msg就是将这个起始地址送到si寄存器中。
接下来print标号处定义了打印相关的功能,mov al, [si]就是取si所代表的内存地址处的一个字节数据,并放到al寄存器中,然后使si加1,指向下一个数据,此时先判断al中的数据是否是0x00,也就是ASCII码‘\0’,这个字符代表字符串的结束,如果确实为0x00,则跳转到last处,让cpu停机。如果不为0x00,则开始准备打印,正如我们上面说到的将0x0e存到ah寄存器中,将0x0f存到bx寄存器中,相当于格式化参数(%c),然后通过int 10来调用中断向量表里面的打印函数,打印完一个字符后继续跳转到print处进行下一次循环,直到到达字符串的末尾。
下面我们讲一讲msg标号处的数据的定义,db伪指令定义一个字节,其后可以是数据,也可以是字符串。db 0x0a即定义一个字节,其中的数据初始化为0x0a,也就是换行符的ASCII码,db "Hello, DTOS!"相当于定义了11个字节。
根据前面内容知道,主引导扇区为512字节大小,而BIOS判断存储介质是否包含主引导扇区的标准就是看看它的前512个字节的最后两个字节是不是0x55和0xaa,如果是,那么BIOS就认为这个存储介质有主引导扇区,接下来就读取主引导扇区到内存中了。
前面写的程序不足以占满512字节,而我们的需求是要将最后两个字节写入0x55和0xaa,怎么办呢?我们让编译器去判断前面程序和数据占用空间的大小,并用适当数量的数据填满前面不足510字节的部分,然后在最后两个字节我们写入0x55和0xaa。
times 510 - ($ - $$) db 0x00这条命令的意思就是填写510 - ($ - $$)个0x00到内存中,$代表times这条伪指令的地址,$$代表当前汇编程序的起始地址(即start处,0x7c00),相减后即为以上所写程序和数据占用的空间大小,再用510减去这个值即为所需要填充的空间的大小。最后,第511字节填入0x55,第512字节填入0xaa。
用到的工具:汇编语言编译器nasm,创建虚拟盘的工具bximage,二进制写入工具dd。由于我们不会将程序真正的刻录到软盘上,因为那样太麻烦了,而且还需要一个真正的物理机器。所以我们使用虚拟盘创建工具bximage,它可以创建一个文件,这个文件存储数据时的格式和软盘是一样的。
下面,我们实际做实验看现象,首先打开ubuntu虚拟机,到工作目录下建立boot.asm文件,敲入上述程序,使用编译器nasm进行编译,生成boot.bin文件,如下所示:
下面创建虚拟盘,执行过程如下图:
这样,虚拟盘就创建好了,名为a.img,这个文件就相当于一个容量1.44M大小的全盘,见下图:
下面我们将boot.bin的内容刻录到这张软盘中,执行命令dd if=boot.bin of=a.img bs=512 count=1 conv=notrunc,结果如下:
至此,这张软盘的前512字节就写入了主引导记录,因此,它就成为了一张启动盘了。
下面使用VmWare创建一个物理机器,详细过程不在贴出,创建好的机器如下,这是一个未安装任何操作系统的空的机器。
我们直接启动这个机器看看效果,如下所示:
从启动界面可以看到,BIOS扫描存储介质时,根本没有发现启动盘,也就是没有找到主引导扇区,因此给出Operating System not found的提示。
接下来,我们将软盘插入这台电脑中,首先将带有主引导扇区的软盘(a.img文件)拷贝到windows系统下,然后将其插入刚才创建的机器中,如下所示:
接下来,给这台机器加电,效果如下:
可见,主引导程序成功被加载并运行了。
小插曲:
主引导程序部分是16位操作,因此,我们需要一个16位的汇编器,现存的汇编器大概有gas汇编器和nasm汇编器,nasm是16位的,用来编译英特尔格式汇编代码,gas是32位的用来编译AT&T格式汇编的,也是唯一支持AT&T格式的汇编器,因此,我们不能选择AT&T格式进行实验。linux的boot程序使用的是as86汇编器进行编译的,这个汇编器是16位的,但是资料比较少。as86是编写Minix操作系统的那个教授编写 的,这个汇编器支持的格式既不是标准的AT&T格式也不是Intel格式,只是和Intel格式比较接近。因此,我们选择了nasm汇编器。
本文参考狄泰软件学院操作系统教程。