[自制操作系统] 第04回 完善MBR
在之前我们说到,MBR的作用便是加载操作系统内核到指定位置。而MBR需要通过读取硬盘来获得操作系统内核。在上一回我们已经讲解了硬盘的工作原理和读取方法,本回便开始初步完善我们的MBR,使之具有读取硬盘的功能。
我们的MBR受限于512字节大小,在这么小的空间没法为内核准备好环境,更没法将内核加载到内存中运行。所以我们需要在另一个程序中完成初始化环境及加载内核的任务,这个程序我们称之为loader,即加载器。loader会在后面的内容实现,问题是,现在我们要考虑loader放在硬盘哪个位置?loader读取出来后加载到内存哪个位置呢?最后MBR又如何跳过去执行?这就是我们MBR最终的使命,也是我们目前需要解决的三个问题。
第一个问题,由于MBR占据了硬盘的第0个扇区(以LBA方式的逻辑来看,扇区从第0开始编号,若是以物理CHS方式的逻辑来看,扇区便是从第1开始编号),第1个扇区是空闲的,但是我们的空间是比较宽裕的,没必要这么省,所以这里就将loader放在第2扇区,后面MBR将它从第2扇区读取出。
第二个问题,loader被加载到内存哪个位置呢?原则上,在内存布局中找一个空闲位置都可以用来存放loader。查看实模式下的内存布局,我们可以看到0x500~0x7BFF和0x7E00~0x9FBFF这两段内存区域都可以。这里提前剧透一下,因为loader中需要定义一些数据结构(比如GDT全局描述符表,这个后面会涉及到),这些数据结构将来的内核依旧会使用到,因此loader被加载到内存后不能被覆盖。其次,随着内核的不断完善,我们的内核会不断变大,所以内核所在的内存地址会向着越来越高的地方增长,难免会超过可用区域的上限,因此我们考虑把loader放在位置低处,至少要放在将来内核加载处的下面,这样也可以尽量多留一些空间给内核。因此最后我们将loader的加载地址放在0x900处,完全是个人的意愿。
第三个问题呢,其实就很简单了,只要我们确定了loader的加载位置,最后只需要在MBR中使用jmp 0x900就可以顺利跳转到loader中去了。
还是首先来看看改写后的MBR代码吧:
1 %include "boot.inc"
2 section MBR vstart=0x7c00
3 mov ax, cs
4 mov ds, ax
5 mov es, ax
6 mov ss, ax
7 mov fs, ax
8 mov sp, 0x7c00
9 mov ax, 0xb800
10 mov gs, ax
11
12 ;利用int 0x10 的0x06号功能实现清屏
13 mov ax, 0x600
14 mov bx, 0x700
15 mov cx, 0
16 mov dx, 0x184f
17
18 int 0x10
19
20 mov ah, 3
21 mov bh, 0
22
23 int 0x10
24 ;输出字符串“HELLO MBR” A表示绿色背景闪烁,4表示前景色为红色
25 mov byte [gs:0x00],'H'
26 mov byte [gs:0x01],0xA4
27
28 mov byte [gs:0x02],'E'
29 mov byte [gs:0x03],0xA4
30
31 mov byte [gs:0x04],'L'
32 mov byte [gs:0x05],0xA4
33
34 mov byte [gs:0x06],'L'
35 mov byte [gs:0x07],0xA4
36
37 mov byte [gs:0x08],'O'
38 mov byte [gs:0x09],0xA4
39
40 mov byte [gs:0x0A],' '
41 mov byte [gs:0x0B],0xA4
42
43 mov byte [gs:0x0C],'M'
44 mov byte [gs:0x0D],0xA4
45
46 mov byte [gs:0x0E],'B'
47 mov byte [gs:0x0F],0xA4
48
49 mov byte [gs:0x10],'R'
50 mov byte [gs:0x11],0xA4
51
52 mov eax, LOADER_START_SECTOR ;起始扇区lba的地址
53 mov bx, LOADER_BASE_ADDR ;loader将要被写入的内存地址
54 mov cx, 4 ;待读入的扇区数
55 call rd_disk_m_16 ;调用函数,将loader写入到内存中
56
57 jmp LOADER_BASE_ADDR
58
59 ;---------------------------------------
60 ;功能:读取硬盘n个扇区
61 rd_disk_m_16:
62 mov esi, eax ;备份eax,eax中存放了扇区号,这里为0x2
63 mov di, cx ;备份cx,cx中存放待读入的扇区数
64
65 ;读写硬盘:
66 ;第一步:设置要读取的扇区数
67 mov dx, 0x1f2
68 mov al, cl
69 out dx, al
70
71 mov eax, esi
72
73 ;第二步:将lba地址存入到0x1f3 ~ 0x1f6
74 ;lba地址7-0位写入端口0x1f3
75 mov dx, 0x1f3
76 out dx, al
77
78 ;lba地址15-8位写入端口0x1f4
79 mov cl, 8
80 shr eax, cl
81 mov dx, 0x1f4
82 out dx, al
83
84 ;lba地址23-16位写入端口0x1f5
85 shr eax, cl
86 mov dx, 0x1f5
87 out dx, al
88
89 shr eax, cl
90 and al, 0x0f
91 or al, 0xe0
92 mov dx, 0x1f6
93 out dx, al
94
95 ;第三步:向0x1f7端口写入读命令,0x20
96 mov dx, 0x1f7
97 mov al, 0x20
98 out dx, al
99
100 ;第四步:检测硬盘状态
101 .not_ready:
102 nop
103 in al, dx
104 and al, 0x88
105 cmp al, 0x08
106 jnz .not_ready
107
108 ;第五步:从0x1f0端口读数据
109 mov ax, di
110 mov dx, 256
111 mul dx
112 mov cx, ax
113 ;di为要读取的扇区数,一个扇区共有512字节,每次读入一个字,总共需要
114 ;di*512/2次,所以di*256
115 mov dx, 0x1f0
116 .go_on_read:
117 in ax, dx
118 mov [bx], ax
119 add bx,2
120 loop .go_on_read
121 ret
122 ;---------------------------------------
123
124 times 510-($-$$) db 0
125 db 0x55, 0xaa
第1行中的%include"boot.inc",这个%include是nasm编译器中的预处理指令,意思就是编译之前将boot.inc文件也包含进来,boot.inc文件存放于当前目录下的include文件夹中。这个文件中目前就只定义了如下两行代码:
1 LOADER_BASE_ADDR equ 0x900 2 LOADER_START_SECTOR equ 0x2
其实就跟我们在C语言中的头文件作用类似,LOADER_BASE_ADDR便是将来loader被加载到内存中的指定位置,LOADER_START_SECTOR是loader存放在硬盘的扇区起始地址。
第2~50行和之前MBR的程序相似,就不再赘述。
下面的rd_disk_m_16便是本次MBR程序改造的重点,也就是硬盘的读取函数。仔细看代码中的步骤,其实就是对照着上一回内容中,硬盘的读取步骤来编写的。最后再将读取到的数据,也就是后面我们会编写的loader加载到内存地址0x900处。
同样使用nasm来编译我们的mbr.S文件,只是由于加入了头文件,因此在编译时需要指定头文件的路径。输入如下代码编译:
nasm -I include/ mbr.S -o mbr.bin
输入如下代码将mbr.bin写入到我们的硬盘第一个扇区,也就是最开始的那个扇区。
dd if=./mbr.bin of=./hd60M.img bs=512 count=1 conv=notrunc
好了,我们已经完成了MBR的修改,接下来需要去创建加载器loader。
其实loader中的内容会比较多,但是目前我们不需要去考虑这些东西,我们这里先实现一个非常非常简单的loader,它的作用就是在屏幕上打印“HELLO LOADER”字符串。我们只是想测试一下MBR中读取硬盘函数和加载功能是否能成功。
同样是在当前目录下新建一个loader.S文件,键入如下代码:
1 %include "boot.inc"
2 section loader vstart=LOADER_BASE_ADDR
3 ;输出背景色为绿色,前景色为红色,并且跳动的字符串“HELLO LOADER”
4 mov byte [gs:0x00],'H'
5 mov byte [gs:0x01],0xA4
6
7 mov byte [gs:0x02],'E'
8 mov byte [gs:0x03],0xA4
9
10 mov byte [gs:0x04],'L'
11 mov byte [gs:0x05],0xA4
12
13 mov byte [gs:0x06],'L'
14 mov byte [gs:0x07],0xA4
15
16 mov byte [gs:0x08],'O'
17 mov byte [gs:0x09],0xA4
18
19 mov byte [gs:0x0A],' '
20 mov byte [gs:0x0B],0xA4
21
22 mov byte [gs:0x0C],'L'
23 mov byte [gs:0x0D],0xA4
24
25 mov byte [gs:0x0E],'O'
26 mov byte [gs:0x0F],0xA4
27
28 mov byte [gs:0x10],'A'
29 mov byte [gs:0x11],0xA4
30
31 mov byte [gs:0x12],'D'
32 mov byte [gs:0x13],0xA4
33
34 mov byte [gs:0x14],'E'
35 mov byte [gs:0x15],0xA4
36
37 mov byte [gs:0x16],'R'
38 mov byte [gs:0x17],0xA4
39 jmp $
同样也是利用nasm和dd命令将汇编文件生成bin文件,随后再写入到硬盘中去。
nasm -I include/ loader.S -o loader.bin dd if=./loader.bin of=./hd60M.img bs=512 count=4 seek=2 conv=notrunc
这里的seek=2的意思就是跳过两个扇区,也就是我们前面说的,将loader存放在磁盘的第0x2个扇区(以LBA法来表示)。
最后让我们开机看看实际效果如何:
这里其实一开始屏幕上方应该是HELLO MBR,随后才是HELLO LOADER。只是一闪而过了,有兴趣的朋友可以去截图看看。总之随着HELLO LOADER的出现,说明我们mbr中的硬盘读写函数是没有问题的,总的框架也没有问题。接下来要做的事就是不断地往loader中填充代码就可以了。本回到此结束,欲知后事如何,请看下回分解。