《操作系统真象还原》chapter2 MBR主引导记录
计算机的启动过程
- 为什么程序要载入内存?
CPU 的硬件电路被设计成只能运行于内存中的程序,这是硬件基因的问题,这样做的原因,首先肯定是内存比较快,且容量大。
其次操作系统可以存储在软盘上,也可以存储在硬盘上,甚至U盘,当然还有很多存储介质都可以。但由于各个硬件特性不同,操作系统要分别考虑每种硬件的特性才行。所以,都在内存中运行程序,操作系统和硬件设计都省事了,这可能也是为了方式的统一吧,
- 什么是载入内存?
所谓的载入内存,大概上分两部分。
1.程序被加载器(软件或硬件)加载到内存某个区域。
2.CPU 的 cs:ip 寄存器被指向这个程序的起始地址。
操作系统在加载程序时,是需要某个加载器来将用户存储到内存中的。其实 “加载器” 这只是人为起的名字,突显了其功能,并不是多么神秘的东西,本质上它就是一堆函数组成的模块,不要因为未知的东西而感到畏惧。
从按下主机上的 power 键后,第一个运行的软件是 BIOS,于是产生了三个问题。
1.它是由谁加载的。
2.它被加载到哪里。
3.它的cs:ip
是谁来更改的。
我们在启动电脑的时候运行的第一个软件是BIOS,但是它是由谁来启动的呢?
软件接力第一棒,BIOS
BIOS 全称叫(Base Input & Output System),即基本输入输出系统。
实模式下的 1MB 内存布局
BIOS使用的是实模式的内存布局。
先从低地址看,地址 0~0x9FFFF处是 DRAM,即动态随机访问内存,我们所装的物理内存就是DRAM,如DDR、DDR2等。
内存地址 0~0x9FFFF 的空间范围是 640KB,这片地址对应到了DRAM,也就是插在主板上的内存条。
顶部的 0xF0000~0xFFFFF,这64KB的内存是ROM。这里存的就是 BIOS 代码。BIOS 的主要工作是检测、初始化硬件。通过调用硬件提供的初始化功能调用来进行初始化。
BIOS 还建立了中断向量表,这样就可以通过 “int 中断号” 来实现相关的硬件调用,BIOS 建立的这些功能就是对硬件的 IO 操作,也就是输入输出,但由于就 64KB 大小的空间,不可能把所有硬件的 IO 操作实现地面面俱到,但 BIOS 也只是在实模式下的基本系统,它只需要胜任它在实模式下的基本使命就够了。剩下的交给保护模式。
内存通过地址总线进行访问,地址总线的宽度决定了可以访问的内存空间大小。但是不止主板撒谎给你的DRAM需要通过地址总线访问,其它例如显存、ROM等也需要通过地址总线访问。
所以物理内存多大都没用,主要是看地址总线的宽度。还要看地址总线的设计,是不是全部用于访问DRAM。
BIOS是如何苏醒的
BIOS是计算机上第一个运行的软件,它又是被谁加载的呢?因为它是第一个运行的软件所以不可能加载它自己,由此可以知道它是由硬件加载的。而这个硬件就是只读存储器 ROM。
ROM 是一块内存,内存就需要被访问。此 ROM 被映射在低端 1MB 内存的顶部,即地址 0xF0000~0xFFFFF 处。只要访问此处的地址便是访问了 BIOS,这个映射是由硬件完成的。
BIOS本身是个程序,程序要执行,就要有个入口地址才行,此入口地址便是 0xFFFF0。
但是 CPU 如何去执行它呢?确切的说就是 CPU 的cs:ip
值是如何组成0xFFFF0的。
BIOS 作为第一个被运行的程序,自然不会是有其它软件启动的它,所以还是由硬件执行完成的。
在开机的一瞬间,也就是接电的时候,CPU 的 cs:ip
寄存器被强制初始化为 0xF000:0xFFF0即地址0xFFFF0。
但是这个地址访问的地方距离内存空间边缘只剩16字节,这样的空间肯定不够 BIOS 完成它的使命,所以 BIOS 真正的代码不在这,此处的代码只能是个跳转指令。
所以地址0xFFFF0
的指令就是一条跳转指令,而这条跳转指令就是jmp far f000,e05b
。即跳转到地址0xfe05b
处,这是 BIOS 代码真正开始的地方。
接下来 BIOS 便开始检测内存、显卡等外设信息,当检测通过,并初始化好硬件后,开始在内存中0x000~0x3FF
处建立数据结构,中断向量表 IVT 并填写中断例程。
为什么是0x7c00
计算机执行到这里 BIOS 也即将完成自己的使命。
BIOS 最后一项工作校验启动盘中位于 0 盘 0 道 1 扇区的内容,即磁盘上最开始那个扇区。
如果扇区的末尾的两个字节分别是魔数0x55
和0xaa
,BIOS 便认为此扇区中确实存在可执行的程序,便加载到物理地址0x7c00
,随后跳转到此地址,继续执行。
这里有一个小细节,BIOS 跳转到0x7c00
是用jmp 0:0x7c00
实现的,这是jmp
指令的直接绝对远转移用法,段寄存器cs
会被替换,这里的段基址是 0,即cs
由之前的0xf000
变成了 0。
至于为什么跳转地址是0x7c00
,是因为历史遗留问题导致的。
而这个地址就是MBR(主引导记录)
MBR就是负责加载启动操作系统的,但是以MBR的程序大小根本无法启动整个操作系统,所以它通过加载一个加载器间接启动操作系统。
让 MBR 先飞一会
编写可以在裸机上运行的MBR程序。
MBR的大小必须是512字节,这是为了保证0x55
和0xaa
这两个魔数恰好出现在该扇区的最后两个字节处,即215字节处和第511字节处,这是按起始偏移为 0 算起的。
$ 和 $ $,section
$
和$$
是编译器 NASM 预留的关键字,用来表示当前行和本section
的地址,起到了标号的作用,跟伪指令差不多就是给编译器识别的。
标号
1 |
|
标号被nasm认为是一个地址,code_start
只是个标记,交给nasm编译器识别,跟伪指令也差不多。
$
属于 “隐式地” 藏在本行代码前的标号,也就是编译器给当前行安排的地址。
1 |
|
上面这行代码跟jmp code_start
是等效的。
$$
指代本section
的起始地址,此地址同样是编译器给安排的。
$
和$$
,默认情况下,它们的值是相对于本文件开头的偏移量。至于实际安排的是多少,还要看是否在section
中添加了vstart
。这个关键字可以影响编译器安排地址的行为,如果该section
用了vstart=xxxx
修饰,$$
的值则是此secton
的虚拟起始地址xxxx
。$
的值是以xxxx
为起始地址的顺延。如果用了vstart
关键字,通过section.节名.start
获得section
在文件中的真实偏移量(真实地址),
如果没有定义section
,nasm默认全部代码同为一个section
,起始地址为0。
section
也是一个伪指令,从名字上就可以知道它的含义是节、段。section
是用于给开发者规划代码用的。可以提高程序的可维护性。
NASM简单用法
1 |
|
以上是nasm的基本用法。
-f
用来指定输出文件的格式。
代码分析
实现功能
用汇编语言编写输出Hello World!
的程序。程序共512字节,最后两个字节是0x55
和0xaa
,中间不足的补0。
代码逻辑
- 清屏
- 获取光标位置
- 在光标位置处打印
Hello World!
代码通过使用0x10
中断(BIOS中断),调用的方法是把功能号送入ah
寄存器,其它参数按照BIOS中断手册的要求放在适当的寄存器中,然后执行int 0x10
即可。
代码实现
1 |
|
将代码保存为mbr.s
文件
之后将代码文件通过nasm编译,然后用dd
命令写入bochs的虚拟硬盘。
1 |
|
写入命令
1 |
|
if
指定要读取的文件,of
指定把数据输出到哪个文件。
bs
是要读写的块大小,这里是要读写一个512字节的块;count
是指定拷贝的块数,这里是1;conv
是指定如何转换文件,这里就不转换。
运行查看效果
bochs -f bochsrc.disk
输出
1 |
|
回车,然后输入c(continue)。
即可看到输出的Hello World!
到这里已经完成了MBR的初步编写