《操作系统真像还原》操作系统实现——MBR

Posted on May 14, 2021

最近这段时间准备学一学 OS,《现代操作系统》一书尝试了很多次都没看进去,还是先找一本轻松一点的书来看,尝试一下能不能跟着这本书写一个简单的 OS 出来。

环境搭建

书上用的虚拟机是 bochs,建议自己编译安装,我第一次编译总是通不过,过了几天就通过了,估计是第一次编译的时候有些依赖没写到环境变量里面导致编译的时候找不到,具体也不是很懂,总之我安装好了就行了。

./configure \  
--prefix=/your/home/.local/ \
--enable-debugger \
--enable-disasm \
--enable-iodebug \
--enable-x86-debugger \
--with-x \
--with-x11

首先设置编译选项,我考虑就直接装到自己的 .local 里了,装在别的地方其实也一样,装到 .local 里面可以省的配环境变量。

然后

make -j$(nproc)

如果 make 没出错就可以执行 make install 安装了,出错了的话就只能自行机智了。

装好之后 bochs 就可以起了,按照书上说的,需要先配置 bochs,由于书中使用的 bochs 不是最新版本,所以使用了最新版本已经弃用的 keyboard_mapping 关键字,需要改为 keyboard。我这里给出我的配置文件

# configuration file for Bochs

# set the maximum RAM bochs can use
# key word: megs
megs: 256

# set the BIOS and VGA BIOS
# key word: `romimage` and `vgaromimage`
romimage: file=/home/chuj/.local/share/bochs/BIOS-bochs-latest
vgaromimage: file=/home/chuj/.local/share/bochs/VGABIOS-lgpl-latest

# set the disk used by bochs
# key word: floppy
# floppya => first disk, floppyb => second disk..
floppya: 1_44=a.img, status=inserted

# select the startup disk
boot: disk # we don't use floppy at all

# set the log file output
log: bochs.out

# disable mouse, enable keyboard
mouse: enabled=0
keyboard: keymap=/home/chuj/.local/share/bochs/keymaps/x11-pc-us.map

# disk setup
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master: type=disk, path="hd60M.img", mode=flat

# add gdb support
# attach to port 1234
# however i didn't complie with this
# gdbstub: enable=1, port=1234, text_base=0, data_base=0, bss_base=0

这里的路径名需要自己设置为安装的路径。同时按照书上建议的,我在编译的时候就没编译进去 gdb 支持,所以这个 gdbstub 选项我就注释掉了。同时注意 29 行,这个是创建硬盘之后要加上的。

然后就可以起了。起了之后立马报错,原因是我们没有启动盘,所以要创建一个,用 bximage 就可以了,书上提供的指令大概也只适用于老版本,所以我是通过直接运行 bximage 来创建的。

硬盘的基础知识

扇区的寻址方式有 CHS(柱面-磁头-扇区)和 LBA(逻辑块地址)方式之分,LBA 对人而言简洁一些,它从零开始计数,这种寻址方式不需要考虑扇区所处的盘面和磁道,而是把硬盘抽象成了一段连续的扇区。

LBA 也分 LBA28 和 LBA48,我们这里先使用 LBA28。

硬盘为了被访问,预留了许多端口,这里不一一列举,查表就可以了。

基础 MBR

BIOS 是硬件厂商写好的,对于 bochs 虚拟机 BIOS 在 install_path/share/bochs/BIOS-bochs-latest,其具体实现不需要过多关心。BIOS 执行完后,会调用 MBR(main boot record)主引导记录来进行进一步的引导。约定磁盘的 0 盘 0 道 1 扇区中存储 MBR,并且该扇区的最后两个字节为 0x55 和 0xaa。

BIOS 准备将下一棒交给 MBR 的时候,会判断这两个字节的魔数,如果不是 0x55 和 0xaa,就会认为这个磁盘不是可引导设备。0 盘 0 道 1 扇区中的数据会被加载到内存地址 0x7C00 中,然后交接时 jmp 过去就可以执行 MBR 了。这里内存地址为什么是 0x7C00,书上说的比较清楚,但是总归是历史原因,而且不是很重要,这里不再赘述。

MBR 要做的事情主要就是寻找并引导内核加载器,即 OBR(OS boot record),这就是我们要实现的 MBR 了。

约定内核加载器处在 0 面 0 道 3 扇区,即 LBA 模式下的第 2 扇区。我们的 mbr 要做的就是把这个扇区的数据都加载到内存中,然后 jmp 过去完成交接。很简单的操作,但是这里涉及到对硬盘的操作,就会麻烦许多。在向硬盘的 command 寄存器中写入指令前我们需要指定 LBA 地址,寻址模式,要读取的扇区数,读取的硬盘。这个的顺序其实无所谓,不过这里还是沿用书中约定的顺序:

  1. 选择通道,并指定要读取的扇区数(事实上通道是通过使用的不同的寄存器来选择的,代码中不会显示的进行选择)。
  2. 指定 LBA 地址的低 24 位。
  3. 写 device 寄存器,设置读取的硬盘和寻址模式(LBA)以及 LBA 地址的高 4 位。
  4. 向 command 寄存器中写入命令。
  5. 读取 status 寄存器,直到硬盘可读。
  6. 读取数据。

书上给出了一个模板,我在此模板上面做了一点微调

mbr.S:

%include "boot.inc"

SECTION MBR vstart=0x7C00
    ; init
    mov ax,cx
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov fs,ax
    mov sp,0x7C00
;    mov sp,STACK
    mov ax,0xb800
    mov gs,ax

    ; clear screen
    mov ax,0x0600                   ; AH:clear. AL:colum to clear, 0 as all
    mov bx,0x0700                   ; BH:colum nature
    mov cx,0                        ; (CL,CH) (x,y) of left-up corner
    mov dx,0x184F                   ; (DL,DH) (x,y) of right-down corner (80-1,25-1)
    int 0x10                        ; use bios interrupt

    ; display "1 MBR"
    mov byte [gs:0x00],'1'
    mov byte [gs:0x01],0xA4

    mov byte [gs:0x02],' '
    mov byte [gs:0x03],0xA4

    mov byte [gs:0x04],'M'
    mov byte [gs:0x05],0xA4

    mov byte [gs:0x06],'B'
    mov byte [gs:0x07],0xA4

    mov byte [gs:0x08],'R'
    mov byte [gs:0x09],0xA4

    ; ready to call kernel loader
    mov esi,LOADER_START_SECTOR     ; LBA addr of start sector
    mov di,LOADER_BASE_ADDR         ; writing addr
    mov dx,1                        ; sectors waiting to read
    call MBR_ReadDiskSector_16
    jmp LOADER_BASE_ADDR           ; call loader
;---------- end of mbr ----------

; function MBR_ReadDiskSector_16(LBA_addr, writing_addr, n), read n sectors from hard-disk in 16 bit mode
; esi: LBA addr of start sector
; di: writing addr
; dx: n
MBR_ReadDiskSector_16:
        ; read sectors
        mov bx,dx                       ; bx keeps the n
        mov al,bl                       ; n sectors
        mov dx,0x1F2                    ; set reg Sector count 
        out dx,al                       ; read n sectors

        ; set LBA addr
        mov eax,esi
        mov dx,0x1F3                    ; set reg LBA low
        out dx,al                       ; write low 8 bits

        mov cl,8
        shr eax,cl
        mov dx,0x1F4                    ; set reg LBA mid
        out dx,al                       ; write LBA mid

        shr eax,cl
        mov dx,0x1F5                    ; set reg LBA high
        out dx,al                       ; write LBA high

        shr eax,cl
        and al,0xF                      ; only 4 bits
        or al,0xE0                      ; 1110b: LBA mode, disk: master
        mov dx,0x1F6                    ; set reg device
        out dx,al                       ; set mode and LBA addr

        ; ready to read
        mov dx,0x1F7                    ; set reg command
        mov al,0x20                     ; mode: read
        out dx,al                       ; do read

        ; check disk status
    .MBR_ReadDiskSector_16_DiskNotReady:
        in al,dx                        ; get disk status
        and al,0x88                     ; result 0x8 => disk is read 
                                        ; result 0x80 => disk is busy
        cmp al,0x08
        jnz .MBR_ReadDiskSector_16_DiskNotReady

        ; read data
        mov ax,bx                       ; get n
        mov dx,256                      ; read by word, so dx = 512 / 2
        mul dx                          ; assum this mul won't overflow
        mov cx,ax                       ; sum of words need to read
        mov dx,0x1F0                    ; set reg data
    .MBR_ReadDiskSector_16_ReadingLoop:
        in ax,dx                        ; read a word
        mov [di],ax                     ; write a word
        add di,2
        loop .MBR_ReadDiskSector_16_ReadingLoop
        ret
; end of function MBR_ReadDiskSector_16

;STACK:
;    times 0x10 dw 0

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

boot.inc:

;---------- loader and kernel ----------
LOADER_BASE_ADDR equ 0x600              ; 0x500 ~ 0x7BFF
LOADER_START_SECTOR que 0x2

loader.S 在这里只是为了证明 mbr 引导成功了而已,所以就不放代码了。

到这里我们基本上写好了 mbr。

最后的显示情况: