2.1 BIOS
当按下开机键的那一刻,发生了什么呢?
这是一个百废待兴的时刻,所有的硬件设备都刚启动,并没有做好准备,甚至连CPU自己都是。此时,就需要一些外力帮助CPU工作起来。具体来说,在CPU刚启动时,其CS:IP
被硬件电路设定为f000:fff0
。这个地址并非指向内存,而是指向主板的一个非易失性ROM,其中存放的代码被称为基本输入/输出系统(Basic Input Output System,BIOS)。这段代码是CPU在加电后运行的第一段代码,其目的是初始化并检测各个硬件设备,如显示器、硬盘、键盘等。
BIOS的执行时间是十分短暂的,其做的最后一件事情是:将硬盘的第一扇区,共512字节,加载到内存地址0x7c00
处,然后通过jmp 0:0x7c00
指令跳转至该处执行。这个扇区中的内容被称为主引导记录(Main Boot Record,MBR)。
BIOS的故事讲完了,但上述描述中有一些细节需要进一步讨论。
2.1.1 实模式
此时的CPU工作在实模式下。所谓实模式,指的是8086这款CPU的工作模式。8086是一款16位的CPU,这意味着其寄存器都是16位的,不过,其地址线却不止16根,而是20根,20根地址线能够达到2的20次方,也就是1M的寻址能力。然而,地址线是多了,寄存器的位数却跟不上,16位的寄存器无法存放20位的地址。于是,8086使用了一个打补丁式的设计:在寻址时,先将段寄存器中存放的地址左移4位,再与偏移地址相加,这样就能强行凑出20位的地址了。
2.1.2 什么叫"硬盘的第一扇区"?
硬盘是由一个或多个圆形的盘堆叠而成的,每个盘上的所有同心圆环构成多个磁道,每个磁道又被分成多个扇区。所以,应该这样描述一个扇区:第x个盘面上的第y个磁道上的第z个扇区。即,用三个数字描述一个扇区。但这种描述方法的缺点显而易见:太麻烦了。于是,人们发明了"逻辑扇区"这一概念,所谓逻辑扇区,就是将整个硬盘视为一个超长数组,其由硬盘中的所有扇区排列形成。从而,只需要一个索引值,就能唯一确定一个扇区了。逻辑扇区由硬盘控制器负责转换,将其对应到某个实际的扇区上。
2.1.3 为什么一个扇区是512字节?
这完全是约定,硬盘的一个扇区就是512字节。
2.1.4 为什么是0x7c00
这个地址
这个地址沿袭自IBM的一款计算机中的BIOS,属于一个约定俗成的地址。0x7c00
这个数刚好是31K,接近32K这个整数,所以可能的原因是:这个BIOS是为有32K内存的计算机准备的,而主引导记录只是一段512字节的过渡代码,要尽量靠上加载,于是,就选用了31K这个位置。
综上,在计算机刚启动时,首先执行的是BIOS中的代码。BIOS在结束之前会加载并跳转到MBR。所以,MBR是操作系统的起点。
2.2 MBR
想要写一个MBR,就需要满足以下要求:
- MBR的大小必须是512字节,不能超过,也不能少,如果少了,就需要凑足512字节
- MBR的最后两字节必须依次是
0x55
和0xaa
,这是MBR与BIOS的约定。只有这么做,BIOS才认为MBR有效,才会加载这个MBR
请看本章代码2/2.1/Mbr.s
。
第1行,声明一个段。这里的vstart=0x7c00
是什么意思呢?这涉及到编译器对汇编代码的编址。在默认情况下,编译器会将段中的所有内容(包括指令和数据)从0开始编址,当使用段中的一个符号时,拿到的便是一个从0开始的偏移地址。但在MBR中,这样做是不对的,不应该从0,而应该从0x7c00
开始编址。请看以下代码:
section mbr
A: db 0
B: db 1
mov ax, A ; 0
mov bx, B ; 1
在MBR中,上述地址是错误的,这两个字节的正确地址应该是0x7c00
和0x7c01
。
最朴素的修正方法是这样:
section mbr
A: db 0
B: db 1
mov ax, 0x7c00 + A ; 0x7c00
mov bx, 0x7c00 + B ; 0x7c01
这样做虽然是对的,但是很麻烦,需要手动调整所有的地址。能从根本上解决问题的方案是:让编译器不要从0开始编址,而是从0x7c00
开始,这就是vstart=0x7c00
的作用:
section mbr vstart=0x7c00 ; 现在,从0x7c00开始编址
A: db 0
B: db 1
mov ax, A ; 0x7c00
mov bx, B ; 0x7c01
第3行,$
是nasm编译器提供的一个特殊标号,用于指代当前行,所以,这一行代码等价于:
this: jmp this
这是一个无限循环,目的是让CPU保持在这一行,不要继续向下执行。现在的目标是让MBR先运行起来,并不做任何事情,所以使用了这一指令。
第5行,times
是nasm提供的一个伪指令,其格式为:
times 一个数字 一段代码
times
伪指令可将后面这段代码重复多次。这里使用times
的目的是将MBR用0填充至510字节,为最后的0x55
和 0xaa
做准备。填充的字节数使用510 - ($ - $$)`进行计算。`$`已经介绍过了,其指代的是当前行的地址;`$$
是nasm提供的另一个特殊标号,其指代的是当前段的起始地址,故$ - $$`表示当前段的大小,`510 - ($ - $$)
表示当前段距离510字节还差多少。
第7行,定义了0x55
和0xaa
,这是BIOS与MBR的约定。
接下来,请看本章代码2/2.1/Makefile
。
第2行,编译Mbr.s
。可以发现,编译好的Mbr
文件确实是512字节。
第3行,将编译好的Mbr
文件写入虚拟硬盘。其中,if=Mbr
用于指定输入文件;of=c.img
用于指定输出文件;seek=0
用于指定写入的起始逻辑扇区,由于MBR必须位于第0个逻辑扇区,所以这里需要使用seek=0
;count=1
用于指定写入的扇区数,MBR的扇区数为1;conv=notrunc
是固定用法。
现在,运行bochs
命令,可以看到虚拟机开始正常运行。
2.3. 文本模式显存
实模式下的1M地址空间并不都是指向物理内存的。具体来说,只有0x0~0x9ffff
指向物理内存,剩下的0xa0000~0xfffff
指向外部设备。在我们的操作系统中,唯一需要关注的外部设备是文本模式显存。
文本模式显存有一个神奇的功能:这段显存里面有什么,屏幕上就显示什么,而且是实时更新的。具体来说,从0xb8000
开始的4000个字节,决定了当前屏幕上显示的内容。bochs使用的是25行,80列的显示模式,每个字符使用两字节,所以一共是4000字节。这两字节中的第一字节为这个字符的ASCII码,第二字节的含义如下:
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
含义 | 是否闪烁 | 背景红色 | 背景绿色 | 背景蓝色 | 是否高亮 | 文字红色 | 文字绿色 | 文字蓝色 |
也就是说,如果想要显示一个最普通的白色字符,第二字节应为0x7
。
请看本章代码2/2.2/Mbr.s
。
第3\~4行,将ds
切换到0xb800
。实模式下的段寄存器在寻址时会左移四位,这样就得到了0xb8000
,这是显存的起始地址。
第6\~7行,通过写显存,在屏幕的左上角打印一个6
。
编译这个新的MBR,并启动bochs,可以看到屏幕的左上角已经打印出了6
。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。