实现一个操作系统~<一>编写MBR

此节github项目地址

计算机的启动过程

当我们按下计算机的power键后,首先运行的就是BIOS,全程为Basic Input/Output System。

BIOS用于电脑开机时运行系统各部分的的自我检测(Power On Self Test),并加载引导程序或存储在主存的操作系统。

由于BIOS是计算机上第一个软件,所以它的启动依靠硬件。

由于BIOS与硬件系统集成在一起(将BIOS程序指令刻录在IC中),所以有时候也被称为固件。在大约1990年BIOS是保存在ROM(只读内存)中而无法被修改。因为BIOS的大小和复杂程度随时间不断增加,而且硬件的更新速度加快,令BIOS也必须不断更新以支持新硬件,于是BIOS就改为存储在EEPROM或者闪存中,让用户可以轻易更新BIOS。

于是BIOS被写进ROM,这块内存被映射在低端1MB内存的顶部,即0xF0000~0xFFFFF。插一句,在实模式下,有20根地址线,因此可以访问1MB的内存空间。来看看实模式下1MB内存的布局:

实现一个操作系统~<一>编写MBR_第1张图片
1.png

这个图很重要,会多次用到!

那么BIOS被启动以后,下一棒要交给谁呢?

BIOS的最后一项工作就是校验启动盘中位于0盘0道1扇区的内容。为什么是1扇区不是0扇区,这是因为CHS方法(Cylinder柱面-Header磁头-Sector扇区)中扇区的编号是从1开始编号的。如果检查到此扇区末尾的两个字节分别是0x550xaa,BIOS就认为此扇区中确实存在可执行程序(此程序便是我们这节讨论的MBR),便加载到物理地址0x7c00,然后跳转到此地址执行。若检查的最后两个字节不是0x55和0xaa,那么就算里面有可执行代码也不能执行了。

当MBR接受了BIOS传来的接力棒,它又做了那些事情呢?

首先了解一下MBR:主引导记录(Master Boot Record,缩写:MBR),又叫做主引导扇区,是计算机开机后访问硬盘时所必须要读取的首个扇区。但是它只有512字节大小,没办法把内核加载到内存并运行,我们要另外实现一个程序来完成初始化和加载内核的任务,这个程序叫做Loader

所以MBR的使命,就是从硬盘把loader加载到内存,就可以把接力棒交给loader了。loader的实现我们下节再讲。

不过还得多说一句,现在我们还在实模式下晃悠。

实模式

我们已经在前面提过了实模式,那么实模式到底是什么,和保护模式又有什么区别?

实模式指的是8086CPU的工作环境,工作方式,工作状态等等这一系列内容。

在最开始的模式里,程序用的地址都是真实的物理地址,“段基址:段内偏移地址”的策略在8086CPU上首次出现,CPU运行环境为16位。

缺点也显而易见,没有对系统级别程序做任何保护,用户程序可以自由访问所以内存;20根地址线,1MB的内存大小远远不够用。

直到32位CPU出现,打破了上述囧境。我们也等以后再具体讨论保护模式。

编写MBR,初见显存

回忆一下前面讲的,BIOS要检测到MBR的最后两个字节为0x55和0xaa,然后才会开始执行MBR中的代码。

首先我们得知道MBR的具体地址,好,前面说过是0x7c00。

那么为什么是这个数字,网上有篇文章解释的很好:

为什么主引导记录的内存地址是0x7C00?

作为一只初学的萌新,我们先不谈让MBR干什么大事,先测试一下能否从BIOS跳到MBR如何?我们的初版MBR的任务就是显示彩色的“Hello MBR”。一旦BIOS能跳转过来,就在屏幕上打印这个字符串。

这就牵扯到了另一个问题?如何在屏幕上显示东西。

如果你对不学计算机的同学提这个问题,怕是要招来异样的目光O(∩_∩)O。还好,大家是同行。在屏幕上显示字符,用专业一点的话来讲就是对显存进行写入。

我们将使用2种方法,1是利用BIOS的中断调用服务,2是直接写入显存

我们都学过计组,因此应该了解ASCII码,而显卡在任何时候都认为你发送的是ASCII码,如果你要发送数字5,应该发送数字5的ASCII码。

显卡的文本模式有多种,在此我使用默认的80*25。每个字符在屏幕上都是用连续的2个字节来表示的,低字节是字符的ASCII码,高字节的低4位是字符前景色,高4位是字符背景色。

实现一个操作系统~<一>编写MBR_第2张图片

K位是闪烁位,0不闪烁,1闪烁。I是亮度位,0正常,1高亮。

实现一个操作系统~<一>编写MBR_第3张图片

BIOS第10h号中断调用

看看维基百科:INT 10H

INT 10h INT 10H 或者 INT 16 是BIOS中断调用的第10H功能的简写, 在基于x86的计算机系统中属于第17中断向量。BIOS通常在此创建了一个中断处理程序提供了实模式下的视频服务。此类服务包括设置显示模式,字符和字符串输出,和基本图形(在图形模式下的读取和写入像素)功能。要使用这个功能的调用,在寄存器AH赋予子功能号,其它的寄存器赋予其它所需的参数,并用指令INT 10H调用。INT 10H的执行速度是相当缓慢的,所以很多程序都绕过这个BIOS例程而直接访问显示硬件。设置显示模式并不经常使用,可以通过BIOS来实现,而一个游戏在屏幕上绘制图形,需要做得很快,所以直接访问显存比用BIOS调用每个像素更适合。

代码:

;mbr.S 调用BIOS 10H号中断
;显示Hello MBR


;-------------------------------------
SECTION  MBR vstart=0x7c00
;vstart作用是告诉编译器,把我的起始地址编为0x7c00

    mov ax, cs    ;用cs寄存器的值初始化别的寄存器
                  ;由于mbr是由jmp 0:0x7c00跳过来的,cs=0
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov fs, ax
    mov sp, 0x7c00

;清屏(向上滚动窗口)
;AH=06H,AL=上滚行数(0表示全部)
;BH=上卷行属性
;(CL,CH)=窗口左上角坐标,(DL,DH)=窗口右下角 
;--------------------------------------
    mov ax, 0x600
    mov bx, 0x700
    mov cx, 0         ;左上角(0,0)
    mov dx, 0x184f    ;右下角(79,24),
                      ;VGA文本模式中一行80个字符,共25行

    int 0x10


;AH=13H 写字符串
;AL=写模式,BH=页码,BL=颜色,CX=字符串长度,DH=行,DL=列,ES:BP=字符串偏移量
;--------------------------------------
    mov ax, message
    mov bp, ax

    mov cx, 9
    mov ax, 0x1301
    mov dx, 0x0909

    mov bx, 0x00EE    ;我的设置为黄色前景棕色背景+闪烁
    int 0x10

;-------------------------------------
    jmp $
    message db "Hello MBR"
    times 510-( $-$$ ) db 0
    db 0x55, 0xaa

程序开头出现了vstart这个词,我来解释一下vstart的意义。

vstart=xxxx的用处就是告诉编译器:你帮我把后面的数据的地址都从xxxx开始编吧。不然的话,编译器会把数据相对文件头的偏移量作为数据的地址,那么就全都是从0开始往后加。

在程序所在目录下执行以下代码,我的程序名为mbr.S,路径为~/MoonOs/C1/boot

nasm -o mbr.bin mbr.S

这句话意思是把mbr.S汇编成纯二进制文件(默认格式)。如果要汇编成别的格式,可参考具体nasm中文手册。

然后执行(注意将每个路径换成你自己的文件路径,硬盘名称(hd.img)也要换成你自己设置的名字)

dd if=/your-path/mbr.bin of=/your-path/hd.img bs=512 count=1 conv=notrunc

dd是Linux下用于磁盘操作的命令,在Linux下man dd即可查看。

上面的命令是:读取mbr.bin,把数据输出到我们指定的硬盘hd.img中,块大小指定为512字节,只操作1块。

然后在bochs目录下执行:

 bochs -f bochsrc

因为我的配置文件为bochsrc,所以你们可以根据自己的情况修改。

最后效果如下:

实现一个操作系统~<一>编写MBR_第4张图片

直接写入显存

无论是哪种显示器,都是由显卡控制的。而无论哪种显卡,都提供了IO端口和显存。显存是位于显卡内部的一块内存。

要往显存里写东西,得先了解显存的布局。

实现一个操作系统~<一>编写MBR_第5张图片

我们使用文本模式,就要从0xB8000开始写入。我们往这块内存里输入的字符会直接落入显存,也就可以显示在屏幕上面了。

;mbr2.S 直接写入显存
;显示Hello MBR


;-------------------------------------
SECTION  MBR vstart=0x7c00
;vstart作用是告诉编译器,把我的起始地址编为0x7c00

    mov ax, cs    ;用cs寄存器的值初始化别的寄存器
                  ;由于mbr是由jmp 0:0x7c00跳过来的,cs=0
    mov ds, ax
    mov ss, ax
    mov fs, ax
    mov sp, 0x7c00
    mov ax, 0xb800
    mov es, ax

;清屏(向上滚动窗口)
;AH=06H,AL=上滚行数(0表示全部)
;BH=上卷行属性
;(CL,CH)=窗口左上角坐标,(DL,DH)=窗口右下角 
;--------------------------------------
    mov ax, 0x600
    mov bx, 0x700
    mov cx, 0         ;左上角(0,0)
    mov dx, 0x184f    ;右下角(79,24),
                      ;VGA文本模式中一行80个字符,共25行

    int 0x10

;直接从(0,0)开始写入
;---------------------------------------
mov byte [es: 0x00], 'H'
mov byte [es: 0x01],  0xEE

mov byte [es: 0x02], 'e'
mov byte [es: 0x03], 0xEE

mov byte [es: 0x04], 'l'
mov byte [es: 0x05], 0xEE

mov byte [es: 0x06], 'l'
mov byte [es: 0x07], 0xEE

mov byte [es: 0x08], 'o'
mov byte [es: 0x09], 0xEE

mov byte [es: 0x0A], ' '
mov byte [es: 0x0B], 0xEE

mov byte [es: 0x0C], 'M'
mov byte [es: 0x0D], 0xEE

mov byte [es: 0x0E], 'B'
mov byte [es: 0x0F], 0xEE

mov byte [es: 0x10], 'R'
mov byte [es: 0x11], 0xEE

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


效果如下:

实现一个操作系统~<一>编写MBR_第6张图片

MBR进阶,使用硬盘

我们的MBR当然不止是在屏幕上显示“hello MBR”就完事了,前面提到过MBR要从硬盘上把Loader加载到内存并且运行,并把接力棒交给它。

也许你会有如下疑问:

为什么要把loader加载入内存?

首先我们要知道MBR和操作系统都是位于硬盘上的。CPU的硬件电路被设计为只能运行处于内存中的程序,因为CPU运行内存中程序更快。所以CPU要从硬盘读取数据,决定把它加载到内存的什么位置。

怎样控制硬盘

CPU只能同IO接口进行交流,那么CPU要和硬盘交流的话,也一定要通过IO接口,硬盘的IO接口就是硬盘控制器。再具体一点,就是硬盘控制器与CPU之间通信是通过端口。所谓端口,其实就是一些位于IO接口中的寄存器。不同的端口有着不同的作用。

实现一个操作系统~<一>编写MBR_第7张图片

可以看到,端口分成了两组,我们重点看Command Block registers。

data寄存器的作用是读取或写入数据,16位(其他寄存器都是8位)。在读硬盘时,硬盘准备好数据后,硬盘控制器将其放在内部的缓冲区中,不断读此寄存器就是在读出缓冲区中的数据。写硬盘时,我们把数据送到此端口,数据就被存入缓冲区,硬盘控制器发现这个缓冲区中有数据了,就把这些数据写入相应扇区。

读硬盘时,端口0x171或0x1F1寄存器叫Error寄存器。只有在读取失败时有用,里面会记录失败信息,尚未读取的硬盘数在Sector count寄存器中。写硬盘时,这个寄存器叫Feature寄存器。用来记录一些参数。

Sector count寄存器用来指定待读取或待写入的扇区数。硬盘每完成一个扇区,就会把此寄存器值减1,如果中间失败了,那这个寄存器的值就是未完成的扇区数。如果它被指定为0,表示要操作256个扇区。

LBA寄存器涉及到LBA方法。

从硬盘读写数据,最经典的就是像硬盘控制器分别发送柱面号,磁头号,扇区号,就是我们前面说过的CHS模式。但是如果把扇区统一编址,把他们看做逻辑扇区,全都是从0开始编号,这样就能节省很多麻烦,这就是LBA方法。

最早的逻辑扇区编址方法是LBA28,用28个比特表示逻辑扇区号。则LBA28可以管理128GB的硬盘。随着硬盘技术的发展,LBA48已经出现,可管理容量达到131072TB。

但是我们为了方便,在这里使用LBA28模式。

LBA寄存器有low mid high 三个,都是8位。但是这三个也只能表示24位,剩下4位被放在device寄存器的低4位。

所以我们可以看出device寄存器是个杂项寄存器。它的第4位用来指定通道上的主盘(o)或从盘(1),第6位用来设置是LBA(1)方式还是CHS(0)方式,第5和7位固定为1。

读硬盘时,端口号为0x1F7或0x177的寄存器是Status,用来给出硬盘状态信息。第0位是ERR位,若此位为1,表情命令出错。第3位是data request位,若为1,表示硬盘已经准备好数据,主机可以把数据读出来了,第7位是BSY位,为1表示硬盘正在忙着,此寄存器中其他位都无效。写硬盘时,它是Command寄存器,把命令写进此寄存器,硬盘就可以开始工作了。

读扇区:0x20

写扇区:0x30

操作步骤如下:

  1. 先选择通道,往该通道的sector count寄存器写入待操作的扇区数
  2. 往该通道上的三个LBA寄存器写入扇区起始地址的低24位
  3. 往device寄存器中写入LBA地址的24~27位,并置第6位为1,使其为LBA模式,设置第4位,选择操作的硬盘(master硬盘或slave硬盘)
  4. 往该通道上的command寄存器写入操作命令
  5. 读取该通道上的status寄存器,判断硬盘工作是否完成
  6. 如果以上步骤是读硬盘,则进入下一个步骤,否则,结束
  7. 将硬盘数据读出

改造MBR

我们的MBR现在在第0扇区(LBA方式),那么不如将loader放在第2扇区,中隔一个扇区安全一些。MBR把loader读出来后,可以选择实模式下1MB的空闲内存存放。回顾前面的图,看到0x500-0x7BFF可用,0x7E00-0x9FBFF可用。因为内核地址增长是从低到高的,所以我们尽量选低地址加载loader,因此选择0x900。

;mbr_disk.S 
;读取硬盘,把loader加载到0x900,并跳转过去

;-------------------------------------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

SECTION  MBR vstart=0x7c00
;vstart作用是告诉编译器,把我的起始地址编为0x7c00

    mov ax, cs    ;用cs寄存器的值初始化别的寄存器
                  ;由于mbr是由jmp 0:0x7c00跳过来的,cs=0
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov fs, ax
    mov sp, 0x7c00
    mov ax, 0xb800
    mov es, ax


;清屏(向上滚动窗口)
;AH=06H,AL=上滚行数(0表示全部)
;BH=上卷行属性
;(CL,CH)=窗口左上角坐标,(DL,DH)=窗口右下角 
;--------------------------------------
    mov ax, 0x600
    mov bx, 0x700
    mov cx, 0         ;左上角(0,0)
    mov dx, 0x184f    ;右下角(79,24),
                      ;VGA文本模式中一行80个字符,共25行

    int 0x10

;直接从(0,0)开始写入
;---------------------------------------
    mov byte [es: 0x00], 'H'
    mov byte [es: 0x01],  0xEE
    
    mov byte [es: 0x02], 'e'
    mov byte [es: 0x03], 0xEE
    
    mov byte [es: 0x04], 'l'
    mov byte [es: 0x05], 0xEE
    
    mov byte [es: 0x06], 'l'
    mov byte [es: 0x07], 0xEE
    
    mov byte [es: 0x08], 'o'
    mov byte [es: 0x09], 0xEE
    
    mov byte [es: 0x0A], ' '
    mov byte [es: 0x0B], 0xEE
    
    mov byte [es: 0x0C], 'M'
    mov byte [es: 0x0D], 0xEE
    
    mov byte [es: 0x0E], 'B'
    mov byte [es: 0x0F], 0xEE
    
    mov byte [es: 0x10], 'R'
    mov byte [es: 0x11], 0xEE
    
    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=LBA起始扇区号
                                    ;bx=数据写入的内存地址
                                    ;cx=读入的扇区数
    mov esi, eax                    ;备份eax,cx
    mov di, cx
;step1:设置读取的扇区数
    mov dx, 0x1f2
    mov al, cl                      ;访问8位端口时,使用寄存器AL
    out dx, al                      ;将AL中的数据写入端口号为0x1f2的寄存器中
                                    ;out目的操作数可以是8位立即数或者寄存器DX
                                    ;源操作数必须为AL或AX
    mov eax, esi

;step2:将LBA地址写入0x1f3-0x1f6(在这里我们的地址就是2)
    ;0x1f3放0-7位
    mov dx, 0x1f3
    out dx, al

    ;0x1f4放8-15位
    mov cl, 8
    shr eax, cl                     ;右移8位
    mov dx, 0x1f4
    out dx, al

    ;0x1f5放16-23位
    shr eax, cl
    mov dx, 0x1f5
    out dx, al

    shr eax, cl
    and al, 0x0f
    or al, 0xe0                     ;设置7-4位为1110,LBA模式,主盘
    mov dx, 0x1f6
    out dx, al

;step3:往Command寄存器写入读命令
    mov dx, 0x1f7
    mov al, 0x20
    out dx, al

;step4:检查硬盘状态
  .not_ready:
    nop
    in al, dx
    and al, 0x88
    cmp al, 0x08
    jnz .not_ready

;step5:从0x1f0端口读出数据
    mov ax, di                     ;DI为要读取的扇区数,data寄存器为16位,即每次读2个字节,要读DI*512/2次
    mov dx, 256
    mul dx                         ;mul指令的被乘数隐含在AX中,乘积的低16位在AX中,高16位存在DX中
    mov cx, ax                     ;把AX的值赋给CX,用作计数器
    
    mov dx, 0x1f0
  .go_on_read:
    in ax, dx                      ;把0x1f0端口读出的数据放在AX寄存器中
    mov [bx], ax                   ;再把AX寄存器中的内容放在偏移地址为BX指向的内存空间
    add bx, 2                      ;一次读1个字
    loop .go_on_read
    ret                            ;记得调用函数后要返回


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



然后在你自己的文件目录下依次执行:

nasm -o mbr_disk.bin mbr_disk.S
dd if=mbr_disk.bin of=/your-path/bochs-2.6.9/hd.img bs=512 count=1 conv=notrunc

我们现在的loader还什么都没干了,那干脆就让它显示“hello you”好了

;loader.S暂时什么也不做,只显示"hello You"

;-------------------------------------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

SECTION LOADER vstart=LOADER_BASE_ADDR
    mov byte [es: 0x00], 'H'
    mov byte [es: 0x01],  0xEE
    
    mov byte [es: 0x02], 'e'
    mov byte [es: 0x03], 0xEE
    
    mov byte [es: 0x04], 'l'
    mov byte [es: 0x05], 0xEE
    
    mov byte [es: 0x06], 'l'
    mov byte [es: 0x07], 0xEE
    
    mov byte [es: 0x08], 'o'
    mov byte [es: 0x09], 0xEE
    
    mov byte [es: 0x0A], ' '
    mov byte [es: 0x0B], 0xEE
    
    mov byte [es: 0x0C], 'Y'
    mov byte [es: 0x0D], 0xEE
    
    mov byte [es: 0x0E], 'o'
    mov byte [es: 0x0F], 0xEE
    
    mov byte [es: 0x10], 'u'
    mov byte [es: 0x11], 0xEE

    jmp $

依次执行:

nasm -o loader.bin loader.S  
dd if=loader.bin of=/your-path/bochs-2.6.9/hd.img bs=512 count=1 seek=2 conv=notrunc

一定要注意这里seek=2,意思是跳过2块,因为我们的Loader在2号扇区。缺少seek的话会出错。

最后在bochs目录下执行:

 bochs -f bochsrc

效果如下:

实现一个操作系统~<一>编写MBR_第8张图片

如果看到“Hello You”,说明程序的确从MBR跳转到了Loader!

下一节我们将会写一个有用的Loader,并介绍保护模式。

你可能感兴趣的:(实现一个操作系统~<一>编写MBR)