实验报告
编写MBR,来实现对win7+ubuntu双系统的启动。大致思路如下:
- 显示系统选择界面
- 根据键盘输入选择启动的系统
- 拉起该操作系统对应的bootloader
使用的工具
虚拟机:QEMU
汇编语言编译器:NASM
二进制文件修改工具:winhex
IDE:vscode
基本流程:在vscode中写好代码,使用NASM将汇编代码转化为二进制文件。使用winhex查看汇编代码的二进制文件,并且修改QEMU的虚拟磁盘。
设置虚拟地址并且初始化寄存器
初始化虚拟地址
SECTION 这是NASM提供的伪代码之一。它的作用就是用来划分代码区域,帮助理解代码。他的存在不会对机器码有什么影响(前提是没有设置vstart,代码中没有使用$ $$ 符号)。而且没有强制的使用限制,你可以随便在代码中插入SECTION
伪代码$,$$。$指本条指令对应的内存地址,例如jmp $ 就是原地跳转,无限循环。$$指本SECTION的起始内存地址,例如jmp $$,就是跳转到该条指令所属的section的起始地址处。
vstart,设置虚拟内存地址。例如vstart=0x7c00,用人话来说:它不是告诉加载器“把这段代码加载到0x7c00处,而是假设加载器会把代码加载到0x7c00处”。为什么要假设呢?还记得上面的$,$$吗?如果没有vstart,那么$$会被NASM翻译成0x0000,而如果vstart=0x7c00,那么$$会被翻译为0x7c00。当然它不仅仅会影响$$,$,也会影响其他东西。
初始化寄存器,代码如下
移动MBR代码
MBR代码会被BIOS加载到内存0x7c00处。那为什么要移动MBR代码到内存其他地方去呢?因为windows的bootloader程序约定为加载至0x7c00处,会覆盖掉我们的MBR代码。
跳转的代码如下:(代码中各个指令的作用可以自行查询)
这里说一下最后一行的作用:当代码移动完成后,我们需要跳转到新代码的下一条指令执行。因此我们在最后一行插入一个标签“new_start”。这个标签没有实际作用,只是指代了一个内存地址(比方说vstart=0x7c00,且这行代码的地址相对于程序开头为0x0020,那么标签指代的地址就是0x7c20。其实这和“$”的作用差不多)。但是我们要注意到
- 编译器编码时,jmp后面跟的地址并不是绝对地址,而是jmp指令所在地址与跳转目标地址的偏移量。例如想要从0x7c00跳到0x7c65,在0x7c00处的jmp指令虽然写的是jmp 0x7c65,不过编译器会将它改为jmp 65。(除非写绝对地址,例如jmp 0x0000:0x7c65,这样可以避免这个问题)
- 程序一经过编译器编译,形成的二进制码就不变了。
由于我们的程序已经在内存中移动了,所以此处如果仅写jmp new_start,而且new_start没有经过上面的修饰,那么程序仍然会在旧的内存段中运行!
正确方法是,我们通过EQU语句,将new_start标签指代的地址改为“与程序开头地址的偏移量“,就是$-$$。然后再加上我们新程序的起始地址。这样就能避免这个问题。即使jmp后面的地址是与目标地址的偏移量。
显示系统选择界面
我们将要调用由BIOS提供的INT 10H中断服务,来与显示屏互动。INT 10H中断服务的使用手册可以在维基百科上查询。
BIOS提供的中断服务会在BIOS运行时写入实模式下的中断向量表中,里面封装一些常用操作,可供调用。(但是这些程序运行缓慢,而且在操作系统接管后很有可能被弃用,所以一般都是在MBR中调用这些中断服务)
清屏
首先,我们将屏幕上的字符清空,方便显示选择菜单(BIOS运行时会在屏幕上显示字符)。
使用INT 10H 的0x06号子功能完成(该功能可以控制屏幕的滚动)
补充知识:字符的显示。可以理解为内存中专门有一段有限空间存储在屏幕上显示的字符。只需要将需要显示的字符写入该段内存中,屏幕上就会显示字符。同时根据字符在内存中的地址,系统会在屏幕上对应的位置显示。所以滚动屏幕其实就是在改变字符在内存中的地址。而如果一个字符被“滚动”出地址范围了,那么这个字符就没了。
显示选择界面
首先我们设置光标位置
其实不设置也行。从上面介绍的字符显示原理,我们不难看出字符显示的位置只和它在内存中的位置有关,和光标没啥关系。不过为了用户体验,光标还是得设置一下的。
代码如下:
紧接着我们显示字符,方式是显示一行字符,然后将光标位置换行,随后再显示一行。
使用INT 10H中的0x13号子功能显示
显示ubuntu字符串的道理同上
读取键盘输入
我们希望实现:根据用户按键,光标上下移动。当用户按下回车,根据光标所在的位置拉起对应的bootloader。
我们使用INT 16H提供的中断服务来读取键盘输入。此外,我们还要实现一个循环,让用户可以多次移动光标。代码如下:
拉起对应的bootloader
首先是我们的ubuntu的bootloader,其实就是大名鼎鼎的GRUB了。想要拉起GRUB,我们就要
- 找到GRUB代码并加载到内存
- 使用跳转指令执行GRUB程序。
那GRUB代码在哪?不同的人电脑不同,总的来说你得自己去看原始的MBR,看看它是去哪边加载的。大部分情况下,GRUB是紧跟着MBR的,就在第二扇区。
GRUB代码应该加载到内存的哪里,阅读MBR源码可知,一般加载到0x8000处。
如何读取磁盘中的GRUB代码?使用INT 13H提供的中断服务。另外要注意,请使用LBA模式读取(就是使用42H的子功能号对应的服务,不要用2H对应的服务)
LBA模式与CHS模式差别:早期的磁盘不管是外磁道还是内磁道,每个磁道上对应的扇区数量是一定的。这样就会出现一个问题,外磁道每一个扇区空间大(物理意义上的大),信息密度低。这导致浪费了许多有效空间。所以后来都是等信息密度设计的磁盘。回到正题:CHS模式下你需要提供磁头号,扇区号,磁道号等信息才能找到磁盘对应的位置。而LBA模式将这个过程简化为:只要提供扇区号(一个维度上寻址)。同时LBA支持更大范围的寻址空间。
代码如下:
注意此处的jmp,我们使用绝对地址,这样可以避免jmp会编译为相对地址,而导致跳转地址的错误。
对于windows我们也是同样的操作
复制硬盘分区表和windows签名
处了上面我们完成的MBR,我们还要通过winhex将硬盘分区表和windows签名复制进生成的二进制文件中。
windows签名是指440号字节后面的四个字节
成果展示
选择界面,可以通过键盘控制光标上下移动(w,s 键)。按下回车可以拉起对应的操作系统
例如这是拉起了GRUB界面