二. 编写mbr,让机器启动起来

mbr简介

大家都知道,在我们按下电脑电源的时候,首先启动的BIOS(基本输入输出系统),那么BIOS又是如何被启动的呢,谁来唤醒他呢,它又在何处运行呢。要了解这些的话,首先得介绍一下我们实模式的内存布局

实模式的内存布局

mark

图中的内容我们现在只需要关注红色框出来的地方,可以看到BIOS的入口地址处只有16BYTE的空间,很显然,这一小块空间肯定存放的不是数据,只能是指令了,图中也写的很明显了

1
jmp f000:e05b

也就是跳转到了(f000 << 4) + e05b = fe05b处,这里的段基址左移四位的原因是,在实模式下段基址寄存器只有16位,想一下,16位的寄存器最多访问2^16=64KB的空间,我们想访问实模式下1MB的空间的话就需要将段基址左移4位,自然就可以访问到1MB的空间了,这么做的原因也是出于兼容性而采取的曲线救国方式,虽然我们现在的OS都已经到了64位,它也还得向下兼容不是吗

当我们的电脑加电的一瞬间cs:ip就会被强制置位f000:e05b了,接下来就对内存,显卡等外设进行检查,做好它的初始化工作之后就完成它的任务了,在最后的时候,BIOS会通过绝对远跳

1
jmp 0:0x7c00

将接力棒交由MBR来加载我们的内核,我们初步的工作就是编写MBR。在进行内核加载之前,我们先通过MBR打印一些字符,来验证我们之前所说是否正确

编写MBR验证程序

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
;主引导程序
;---------------------

SECTION MBR vstart=0x7c00 ;程序开始的地址
mov ax, cs ;使用cs初始化其他的寄存器
mov ds, ax ;因为是通过jmp 0:0x7c00到的MBR开始地址
mov es, ax ;所以此时的cs为0,也就是用0初始化其他寄存器
mov ss, ax ;此类的寄存器不同通过立即数赋值,采用ax中转
mov fs, ax
mov sp, 0x7c00 ;初始化栈指针

;清屏利用0x10中断的0x6号功能
;------------------------
mov ax, 0x600
mov bx, 0x700
mov cx, 0
mov dx, 0x184f

int 0x10
;获取光标位置
;---------------------
mov ah, 3 ; 3号子功能获取光标位置
mov bh, 1 ; bh寄存器存储带获取光标位置的页号,从0开始,此处填1可以看成将光标移动到最开始
int 0x10

;打印字符串
;------------------
mov ax, message
mov bp, ax

mov cx, 6 ;字符串长度,不包括'\0'
mov ax, 0x1301
mov bx, 0x2

int 0x10

jmp $

message db "My MBR"
times 510-($-$$) db 0
db 0x55, 0xaa

这段代码通过0x10号中断直接操控显卡,达到打印字符串的目的

编写好后通过

1
2
nasm -o mbr.bin mbr.S
dd if=mbr.bin of=/home/ba/bochs/hd60M.img bs=512 count=1 conv=notrunc

对我们的汇编代码进行编译并写入之前创建的磁盘中,接下来运行bochs,应该可以看到如下结果

mark

现在我们通过bochs的调试看一下程序到底是怎么执行的,和我们之前所说的是否一致

mark

这幅图是在我们开启bochs时显示的结果,很明显可以看到他的cs:ip寄存器的值和我们之前所说的结果一致,在这里进行跳转之后接下来肯定就是一系列的初始化工作了,我们跳过这些初始化的工作,直接进入到MBR执行的开始位置,也就是地址0x7c00处

mark

可以看到,左边是bochs初始化完成之后的输出,这是已经运行到了0x7c00后的结果,看红框标记的地方,有没有感觉很熟悉,这就是我们mbr的第一行代码啦,接下来就会按照我们所写的那样,清屏,打印了。

读取硬盘

前面通过打印字符串对开机启动过程做了个小小的验证,接下来需要让我们的MBR读取硬盘啦,因为加载kernel的话肯定需要从硬盘中读入数据

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
; 主引导程序
;---------------------

%include "boot.inc"

SECTION MBR vstart=0x7c00 ;程序开始的地址
mov ax, cs ;使用cs初始化其他的寄存器
mov ds, ax ;因为是通过jmp 0:0x7c00到的MBR开始地址
mov es, ax ;所以此时的cs为0,也就是用0初始化其他寄存器
mov ss, ax ;此类的寄存器不同通过立即数赋值,采用ax中转
mov fs, ax
mov sp, 0x7c00 ;初始化栈指针,sp也就是32位下的esp

;清屏利用0x10中断的0x6号功能
;------------------------
mov ax, 0x600
mov bx, 0x700
mov cx, 0
mov dx, 0x184f

int 0x10

;获取光标位置
;---------------------
mov ah, 3 ; 3号子功能获取光标位置
mov bh, 1 ; bh寄存器存储带获取光标位置的页号,从0开始,此处填1可以看成将光标移动到最开始
int 0x10

;打印字符串`
;------------------
mov ax, message
mov bp, ax

mov cx, 6
mov ax, 0x1301
mov bx, 0x2

int 0x10

message db "My MBR"

mov eax, LOADER_START_SECTOR ;起始扇区的lba地址
mov bx, LOADER_BASE_ADDR ;写入的地址
mov cx, 1 ;读入的扇区数
call rd_disk_m_16

jmp LOADER_BASE_ADDR

;读取n个扇区
;---------------------
rd_disk_m_16: ;eax=扇区号,cx=读入的扇区数,bx=将数据写入的内存地址
mov esi, eax ;备份eax和cx
mov di, cx

;设置要读取的扇区数
mov dx, 0x1f2
mov al, al
out dx, al
mov eax, esi

;将lba地址存入0x1f3-0x1f6

;lba地址0-7位写入端口0x1f3
mov dx, 0x1f3
out dx, al

;lba地址8-15位写入端口0x1f4
mov cl, 8
shr eax, cl
mov dx, 0x1f4
out dx, al

;lba地址16-23位写入端口0x1f5
shr eax, cl
mov dx, 0x1f5
out dx, al


shr eax, cl
and al, 0x0f
or al, 0xe0
mov dx, 0x1f6
out dx, al

;向0x1f7端口写入读命令
mov dx, 0x1f7
mov al, 0x20
out dx, al
.not_ready:
nop
in al, dx
and al, 0x88 ;第4位为1表示硬盘控制器已经准备号数据传输,第7位为1表示硬盘忙
cmp al, 0x08
jnz .not_ready

;从0x1f0端口读数据
mov ax, di
mov dx, 256
mul dx
mov cx, ax
mov dx, 0x1f0
.go_on_read:
in ax, dx
mov [bx], ax
add bx, 2
loop .go_on_read
ret

times 510-($-$$) db 0
db 0x55, 0xaa

这是boot.inc中的内容

1
2
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

先看看程序的执行流程

  1. 从0x7c00入口处进入mbr
  2. 打印My MBR
  3. 为读取磁盘操作传递参数,包括读入的扇区数,读取的数据写入的内存地址
  4. 将读取到的数据写入0x900,并跳到此处去执行

MBR中的内容差不多就多了,接下来的工作就是逐步完善内核。