《操作系统真象还原》chapter2 MBR主引导记录


计算机的启动过程

  1. 为什么程序要载入内存?

CPU 的硬件电路被设计成只能运行于内存中的程序,这是硬件基因的问题,这样做的原因,首先肯定是内存比较快,且容量大。

其次操作系统可以存储在软盘上,也可以存储在硬盘上,甚至U盘,当然还有很多存储介质都可以。但由于各个硬件特性不同,操作系统要分别考虑每种硬件的特性才行。所以,都在内存中运行程序,操作系统和硬件设计都省事了,这可能也是为了方式的统一吧,

  1. 什么是载入内存?

所谓的载入内存,大概上分两部分。

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 扇区的内容,即磁盘上最开始那个扇区。

如果扇区的末尾的两个字节分别是魔数0x550xaa,BIOS 便认为此扇区中确实存在可执行的程序,便加载到物理地址0x7c00,随后跳转到此地址,继续执行。

这里有一个小细节,BIOS 跳转到0x7c00是用jmp 0:0x7c00实现的,这是jmp指令的直接绝对远转移用法,段寄存器cs会被替换,这里的段基址是 0,即cs由之前的0xf000变成了 0。

至于为什么跳转地址是0x7c00,是因为历史遗留问题导致的。

而这个地址就是MBR(主引导记录)

MBR就是负责加载启动操作系统的,但是以MBR的程序大小根本无法启动整个操作系统,所以它通过加载一个加载器间接启动操作系统。

让 MBR 先飞一会

编写可以在裸机上运行的MBR程序。

MBR的大小必须是512字节,这是为了保证0x550xaa这两个魔数恰好出现在该扇区的最后两个字节处,即215字节处和第511字节处,这是按起始偏移为 0 算起的。

$ 和 $ $,section

$$$是编译器 NASM 预留的关键字,用来表示当前行和本section的地址,起到了标号的作用,跟伪指令差不多就是给编译器识别的。

标号

1
2
code_start:
mov ax,0

标号被nasm认为是一个地址,code_start只是个标记,交给nasm编译器识别,跟伪指令也差不多。

$属于 “隐式地” 藏在本行代码前的标号,也就是编译器给当前行安排的地址。

1
2
code_start
jmp $

上面这行代码跟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 <format><filename> [-o <output>]

以上是nasm的基本用法。

-f用来指定输出文件的格式。

代码分析

实现功能

用汇编语言编写输出Hello World!的程序。程序共512字节,最后两个字节是0x550xaa,中间不足的补0。

代码逻辑

  • 清屏
  • 获取光标位置
  • 在光标位置处打印Hello World!

代码通过使用0x10中断(BIOS中断),调用的方法是把功能号送入ah寄存器,其它参数按照BIOS中断手册的要求放在适当的寄存器中,然后执行int 0x10即可。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
                                ;主引导程序 
;------------------------------------------------------------
SECTION MBR vstart=0x7c00
mov ax,cs ;此时cs寄存器为0,自然可以用来将ax寄存器置0
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
; 清屏 利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0x600 ;ah中输入功能号
mov bx, 0x700 ;设置上卷行属性,0x07表示用黑底白字的属性填充空白行
mov cx, 0 ;左上角: (0, 0)
mov dx, 0x184f ;右下角: (80,25)
;VGA文本模式中,一行只能容纳80个字符,共25行。
;下标从0开始,所以0x18=24,0x4f=79
int 0x10 ;int 0x10

;;;;;;;;; 下面这三行代码是获取光标位置 ;;;;;;;;;
mov ah, 3 ;输入: 3号子功能是获取光标位置,需要存入ah寄存器
mov bh, 0 ;bh寄存器存储的是待获取光标的页号

int 0x10 ;输出: ch=光标开始行,cl=光标结束行
;dh=光标所在行号,dl=光标所在列号

;;;;;;;;; 获取光标位置结束 ;;;;;;;;;;;;;;;;

;;;;;;;;; 打印字符串 ;;;;;;;;;;;
;还是用10h中断,不过这次是调用13号子功能打印字符串
mov ax, message
mov bp, ax ; es:bp 为串首地址, es此时同cs一致,
; 开头时已经为sreg初始化

; 光标位置要用到dx寄存器中内容,cx中的光标位置可忽略
mov cx, 0xc ; cx 为串长度,不包括结束符0的字符个数
mov ax, 0x1301 ; 子功能号13是显示字符及属性,要存入ah寄存器,
; al设置写字符方式 ah=01: 显示字符串,光标跟随移动
mov bx, 0x2 ; bh存储要显示的页号,此处是第0页,
; bl中是字符属性, 属性黑底绿字(bl = 02h,07是黑底白字)
int 0x10 ; 执行BIOS 0x10 号中断
;;;;;;;;; 打字字符串结束 ;;;;;;;;;;;;;;;

jmp $ ; 使程序悬停在此

message db "Hello World!"
times 510-($-$$) db 0
db 0x55,0xaa

将代码保存为mbr.s文件

之后将代码文件通过nasm编译,然后用dd命令写入bochs的虚拟硬盘。

1
nasm mbr.s -o test

写入命令

1
dd if=/root/test of=/root/bochs/hd60M.img bs=512 count=1 conv=notrunc

if指定要读取的文件,of指定把数据输出到哪个文件。

bs是要读写的块大小,这里是要读写一个512字节的块;count是指定拷贝的块数,这里是1;conv是指定如何转换文件,这里就不转换。

运行查看效果

bochs -f bochsrc.disk

输出

1
2
3
4
5
6
7
8
9
1. Restore factory default configuration
2. Read options from...
3. Edit options
4. Save options to...
5. Restore the Bochs state from...
6. Begin simulation
7. Quit now

Please choose one: [6]

回车,然后输入c(continue)。

即可看到输出的Hello World!

到这里已经完成了MBR的初步编写

后言

参考链接:用《操作系统真象还原》写一个操作系统 第二章 编写MBR主引导记录,让我们开始掌权