[自制操作系统] 第04回 完善MBR

目录
一、前景回顾
二、改写MBR
三、实现loader

 

一、前景回顾

  在之前我们说到,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
mbr.S

  第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中的内容会比较多,但是目前我们不需要去考虑这些东西,我们这里先实现一个非常非常简单的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 $
loader.S

  同样也是利用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中填充代码就可以了。本回到此结束,欲知后事如何,请看下回分解。

posted @ 2022-06-05 22:00  李知行  阅读(484)  评论(0编辑  收藏  举报