《ORANGE’S:一个操作系统的实现》读书笔记(三十八)尾声(二)

这篇文章是尾声的第二部分,记录如何将Orange’S安装到硬盘上,并实现从硬盘启动。

目录

从硬盘引导

编写硬盘引导扇区和硬盘版loader

“安装”hdboot.bin和hdldr.bin

grub

小结


从硬盘引导

虽然我们的硬盘上已经有不少内容了,但到目前为止,我们的系统始终是从软盘启动的。下面我们要做的事情就是将Orange’S安装到硬盘上,并实现硬盘启动。

我们先回忆一下从软盘启动的过程:

  1. BIOS将引导扇区读入内存0000:7c00处;
  2. 跳转到0000:7c00处开始执行引导代码;
  3. 引导代码从软盘中找到loader.bin,并将其读入内存;
  4. 跳转到loader.bin开始执行;
  5. loader.bin从软盘中找到kernel.bin,并将其读入内存;
  6. 跳转到kernel.bin开始执行,到此可认为启动过程结束;
  7. 系统运行中。

在第1步中,BIOS到底读软盘还是硬盘是由CMOS设置决定的,通常你可以找到一个叫做“Boot Sequence”的选项,从中选择首选启动设备。在第3步和第5步中,对于软盘启动,代码将在软盘中寻找loader.bin和kernel.bin,对于硬盘启动,我们需要让引导扇区代码从硬盘中寻找loader.bin并让loader从硬盘中寻找kernel.bin。这便是软盘和硬盘启动的区别了。剩下的几步中,软盘和硬盘启动没有分别。

因此我们需要重写boot.asm和loader.asm,让它们读取硬盘而不是软盘。新的文件我们起名为hdboot.asm和hdldr.asm。

编写硬盘引导扇区和硬盘版loader

我们先来完成hdboot.asm。它跟boot.asm的区别主要在两方面,一是读取软盘和硬盘扇区的方法有所不同;二是软盘的文件系统(FAT12)跟硬盘的文件系统(Orange’S FS)不同,所以寻找loader的方式肯定也不一样。

我们在之前的记录中讲到如何用int 13h读取软盘扇区,实际上这个中断也可用来读取硬盘,见下表。

中断号寄存器作用
13hah: 02hal: 要读扇区数从磁盘读数据入es:bx指向的缓冲区中
ch: 柱面号低八位

cl:            第0~5位:起始扇区号

               第6~7为:柱面号高二位

dh: 磁头号dl: 驱动器号(置第7位 表示硬盘操作)

根据这个表我们知道,以这种方式读取磁盘,允许的磁头数最大为256,柱面数最大为1024,扇区数最大为64,所以容易算出所支持的硬盘最多有16777216(256*1024*64)个扇区,合8GB。这实在是个太严重的限制了,因为目前的硬盘很少小于8GB,显然这个方法读取硬盘不太好。

好在我们并不是最先不满于这个限制的第一批人,West Digital和Phoenix Technologies联合推出了EDD标准(BIOS Enhanced Disk Drive Services),它支持64位LBA。它的具体做法是将原来放进寄存器的参数放进内存中的数据结构,这个数据结构叫做Disk Address Packed,见下表。

偏移位数描述
08Packet有多少字节
18保留
28要传输的块数(最大值为127;0表示不传输数据)
38保留
432读操作的目的地址(段:偏移)
864LBA地址

当读磁盘扇区时,只需要将AH设为42h,DL设为驱动器号,DS:SI设为Disk Address Packet的地址,然后调用int 13h就可以了。

了解了读取硬盘的方法,我们就可以开始写hdboot.asm了,代码如下所示。

代码 boot/hdboot.asm。

org 0x7c00      ; bios always loads boot sector to 0000:7C00

    jmp boot_start

%include "load.inc"

STACK_BASE      equ 0x7C00      ; base address of stack when booting
TRANS_SECT_NR   equ 2
SECT_BUF_SIZE   equ TRANS_SECT_NR * 512

disk_address_packet:    db 0x10             ; [ 0] Packet size in bytes.
                        db 0                ; [ 1] Reserved, must be 0.
                        db TRANS_SECT_NR    ; [ 2] Nr of blocks to transfer.
                        db 0                ; [ 3] Reserved, must be 0.
                        dw 0                ; [ 4] Addr of transfer - Offset
                        dw SUPER_BLK_SEG    ; [ 6] buffer.          - Seg
                        dd 0                ; [ 8]  LBA. Low    32-bits.
                        dd 0                ; [ 12] LBA. High   32-bits.

err:
    mov dh, 3       ; "Error 0 "
    call disp_str   ; display the string
    jmp $

boot_start:
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, STACK_BASE

    call clear_screen

    mov dh, 0           ; "Booting  "
    call disp_str       ; display the string

    ; read the super block to SUPER_BLK_SEG::0
    mov dword [disk_address_packet + 8], ROOT_BASE + 1
    call read_sector
    mov ax, SUPER_BLK_SEG
    mov fs, ax

    mov dword [disk_address_packet + 4], LOADER_OFF
    mov dword [disk_address_packet + 6], LOADER_SEG

    ; get the sector nr of '/' (ROOT_INODE), it'll be stored in eax
    mov eax, [fs:SB_ROOT_INODE]
    call get_inode

    ; read '/' into es:bx
    mov dword [disk_address_packet + 8], eax
    call read_sector

    ; let's search '/' for the loader
    mov si, LoaderFileName
    push bx     ; <- save
.str_cmp:
    ; before comparation:
    ;   es:bx -> dir_entry @ disk
    ;   ds:si -> filename we want
    add bx, [fs:SB_DIR_ENT_FNAME_OFF]
.1:
    lodsb                   ; ds:si -> al
    cmp al, byte [es:bx]
    jz .2
    jmp .different          ; oops
.2:                         ; so far so good
    cmp al, 0               ; both arrive at a '\0', match
    jz .found
    inc bx                  ; next char @ disk
    jmp .1                  ; on and on
.different:
    pop bx                  ; -> restore
    add bx, [fs:SB_DIR_ENT_SIZE]
    sub ecx, [fs:SB_DIR_ENT_SIZE]
    jz .not_found

    mov dx, SECT_BUF_SIZE
    cmp bx, dx
    jge .not_found

    push bx
    mov si, LoaderFileName
    jmp .str_cmp
.not_found:
    mov dh, 2
    call disp_str
    jmp $
.found:
    pop bx
    add bx, [fs:SB_DIR_ENT_INODE_OFF]
    mov eax, [es:bx]        ; eax <- inode nr of loader
    call get_inode          ; eax <- start sector nr of loader
    mov dword [disk_address_packet + 8], eax
load_loader:
    call read_sector
    cmp ecx, SECT_BUF_SIZE
    jl .done
    sub ecx, SECT_BUF_SIZE          ; bytes_left -= SECT_BUF_SIZE
    add word [disk_address_packet + 4], SECT_BUF_SIZE   ; transfer buffer
    jc err
    add dword [disk_address_packet + 8], TRANS_SECT_NR  ; LBA
    jmp load_loader
.done:
    mov dh, 1
    call disp_str
    jmp LOADER_SEG:LOADER_OFF
    jmp $

; 字符串
LoaderFileName      db  "hdldr.bin", 0  ; LOADER 文件名
; 为简化代码,下面每个字符串的长度均为 MessageLength
MessageLength       equ 9
BootMessage:        db  "Booting  "     ; 9字节,不够则用空格补齐. 序号 0
Message1            db  "HD Boot  "     ; 9字节,不够则用空格补齐. 序号 1
Message2            db  "No LOADER"     ; 9字节,不够则用空格补齐. 序号 2
Message3            db  "Error 0  "     ; 9字节,不够则用空格补齐. 序号 3

clear_screen:
    mov ax, 0x600       ; AH = 6, AL = 0
    mov bx, 0x700       ; 黑底白字(BL = 0x7)
    mov cx, 0           ; 左上角: (0, 0)
    mov dx, 0x184f      ; 右下角: (80, 50)
    int 0x10            ; int 0x10
    ret

; 函数名:disp_str
; 作用:
;   显示一个字符串,函数开始时 dh 中应该是字符串序号(0-based)
disp_str:
    mov ax, MessageLength
    mul dh
    add ax, BootMessage
    mov bp, ax              ; ┓
    mov ax, ds              ; ┣ ES:BP = 串地址
    mov es, ax              ; ┛
    mov cx, MessageLength   ; CX = 串长度
    mov ax, 01301h          ; AH = 13,  AL = 01h
    mov bx, 0007h           ; 页号为0(BH = 0) 黑底白字(BL = 07h)
    mov dl, 0
    int 10h                 ; int 10h
    ret

; read_sector
; Entry:
;   - fields disk_address_packet should have been filled before invoking the routine
; Exit:
;   - es:bx -> data read
; registers changed:
;   - eax, ebx, dl, si, es
read_sector:
    xor ebx, ebx

    mov ah, 0x42
    mov dl, 0x80
    mov si, disk_address_packet
    int 0x13

    mov ax, [disk_address_packet + 6]
    mov es, ax
    mov bx, [disk_address_packet + 4]

    ret

; get_inode
; Entry:
;   - eax       : inode nr.
; Exit:
;   - eax       : sector nr.
;   - ecx       : the_inode.i_size
;   - es:ebx    : inodes sector buffer
; registers changed:
;   - eax, ebx, ecx, edx
get_inode:
    dec eax                     ; eax <- inode_nr - 1
    mov bl, [fs:SB_INODE_SIZE]
    mul bl                      ; eax <- (inodd_nr - 1) * INODE_SIZE
    mov edx, SECT_BUF_SIZE
    sub edx, dword [fs:SB_INODE_SIZE]
    cmp eax, edx
    jg err
    push eax

    mov ebx, [fs:SB_NR_IMAP_SECTS]
    mov edx, [fs:SB_NR_SMAP_SECTS]
    lea eax, [ebx + edx + ROOT_BASE + 2]
    mov dword [disk_address_packet + 8], eax
    call read_sector

    pop eax                     ; [es:ebx+eax] -> the inode

    mov edx, dword[fs:SB_INODE_ISIZE_OFF]
    add edx, ebx
    add edx, eax                ; [es:edx] -> the_inode.i_size
    mov ecx, [es:edx]           ; ecx <- the_inode.i_size

    add ax, word[fs:SB_INODE_START_OFF]

    add bx, ax
    mov eax, [es:bx]
    add eax, ROOT_BASE          ; eax <- the_inode.i_start_sect
    ret

times 510 - ($-$$) db 0         ; 填充剩下的空间,使生成的二进制代码恰好为512字节
dw 0xaa55                       ; 结束标志

代码的主干部分逻辑还是比较清晰的,所做工作依次是读取超级块,读取根目录,从根目录中寻找hdldr.bin,将hdldr.bin读入内存并将控制权交给它。在整个过程中有两个函数被调用多次:是read_sector和get_inode。

read_sector默认disk_address_packet已被填充完毕,所以它只是为AH和DL赋值,然后调用int 13h,这样指定扇区就被读入到Disk Address Packet指定的内存了。为使用方便,read_sector之后让es:bx指向刚刚读入的内存地址,这样调用者通过es:bx访问就可以了。

举例说明,在第38行之前,Disk Address Packet中已经准备好了读操作的目的地址和要读取的扇区数,所以第38行我们给LBA赋值之后,就可以调用read_sector了(第39行),这样超级块就被读入到SUPER_BLK_SEG:0处。接下来第40行和第41行让fs段指向SUPER_BLK_SEG,这样我们就可以通过fs方便地访问超级块了。比如第47行就从超级块中读出根inode号,从而用get_inode得到根目录的首扇区号。

get_inode用来获取给定i-node对应的文件的首扇区号。它的输入是inode号(保存在eax中),输出有二,分别是文件的首扇区号(保存在eax中)和文件长度(保存在ecx中)。

对于Disk Address Packet这个结构,在第16行我们将数据传输目的段地址设为SUPER_BLK_SEG,因为我们首先要读取超级块。SUPER_BLK_SEG在load.inc中被设为0x70,也就是说超级块将被读到0x700~0x8FF处。之后在第43行和第44行将数据传输的目的地址设成了LOADER_SEG:LOADER_OFF。从这之后除LBA之外,Disk Address Packet结构的其余各项基本不再改变,也就是说,在调用read_sector之前,只需要关注LBA就可以了。

注意LOADER_SET:LOADER_OFF这块内存被复用多次。在第48行和93行调用get_inode时,这块内存用于保存inode_array中的扇区内容。而在第52行调用read_sector时,这块内存用于保存根目录内容。只有到了第96行,LOADER_SEG:LOADER_OFF这块内存才真正“名副其实”地用来保存hdldr.bin的文件内容。

此外需要留意以下几个方面:

  1. ROOT_BASE以宏的方式定义,所以如果更换磁盘或者分区,hdboot.asm需要重新编译。之所以这样做,是因为对于某个特定的系统,这个值是固定的,而且通过宏来“指定”它,而不是通过分区表来“计算”它,将大大简化hdboot.asm的代码。
  2. 代码中用到了若干的宏,比如SB_INODE_SIZE、SB_NR_IMAP_SECTS等,它们需要和fs.h中super_block结构一致。
  3. read_sector中,寄存器DL总是被赋值为0x80,也就是说,这个引导扇区默认我们的操作系统安装在第一块硬盘上。
  4. 并不是所有的i-node都能被get_inode支持。inode_array可能很长,但我们只读取最开始的一段,具体由TRANS_SECT_NR决定。也就是说,如果所请求的i-node位于inode_array的前TRANS_SECT_NR个扇区之外,get_inode将提示错误。这样做是有理由的,因为get_inode只用于得到“/”和“hdldr.bin”两个文件的i-node,而“/”通常占用第一个i-node,而hdldr.bin在系统被安装时就理应存在于文件系统之内,所以它的i-node也应该是很靠前的,因此get_inode不会出错。
  5. 跟第4条情况类似,对根目录的遍历也是有限的,但由于hdldr.bin理应位于目录的前部,所以只要hdldr.bin存在,它就应该能被找到。

以上便是硬盘引导扇区的情况了。接下来我们就可以修改loader.asm,形成一个hdldr.asm是很容易的事情了。需要改的只是读取kernel.bin的部分,而这一部分跟hdboot.asm中读取hdldr.bin的代码基本一致,所以不再赘述(代码位于 boot/hdldr.asm)。

“安装”hdboot.bin和hdldr.bin

下面我们将hdboot.asm和hdldr.asm编译成hdboot.bin和hdldr.bin,然后将它们“安装”到系统中。

安装hdboot.bin也就是将它放入硬盘引导扇区中,这可以使用dd命令:

dd if=boot/hdboot.bin of=hd.img bs=1 count=446 conv=notrunc
dd if=boot/hdboot.bin of=hd.img seek=510 skip=510 bs=1 count=2 conv=notrunc

注意这里使用了两个dd命令,这是为了不至于覆盖掉原有的分区表——如果存在的话。

安装hdldr.bin看上去不如hdboot.bin那么直接,因为它将以普通文件的身份存在于Orange’S FS。不要担心,我们可以将它打进cmd.tar这个包,然后用软盘启动一下,它就老老实实地被解压到文件系统中去了。

注意若想从硬盘启动,之前的一次软盘启动时免不了的。软盘启动主要做两件事情:

  • 通过mkfs()将硬盘的相应分区做成Orange’S FS
  • 将cmd.tar解开,这时FS中就有hdldr.bin和kernel.bin了。

安装好了引导扇区和loader,系统就可以启动了,别忘了先将bochsrc中的启动设备改成硬盘,运行结果如下图所示。

成功了!而且从打印的字符串可以清楚地看到,这时我们的hdboot.bin和hdldr.bin在运行了。

grub

系统已经可以在硬盘上运行了,不过我们仍然可以改进一下,将引导扇区安装到Orange’S分区的引导扇区,而不是整块硬盘的引导扇区,这样Orange’S就可以跟硬盘上其它操作系统和平共处了。做到这一点其实很容易,只需要安装一个grub就可以了。

我们先将引导扇区装到Orange’s分区的引导扇区:

dd if=boot/hdboot.bin of=hd.img seek=`echo "obase=10;ibase=16;\`egrep -e '^ROOT_BASE' boot/include/load.inc | sed -e 's/.*0x//g' | sed -e 's/^M//g'\`*200" | bc` bs=1 count=446 conv=notrunc
dd if=boot/hdboot.bin of=hd.img seek=`echo "obase=10;ibase=16;\`egrep -e '^ROOT_BASE' boot/include/load.inc | sed -e 's/.*0x//g' | sed -e 's/^M//g'\`*200+1FE" | bc` skip=510 bs=1 count=2 conv=notrunc

其中dd命令的seek参数的值仍然使用一个组合命令从load.inc中得到。

接下来该grub上场了。如果硬盘上已经有一个Linux发行版,那么在它当初安装时很可能已经装上grub了。现在的Linux发行版安装的Grub是GRUB 2,而之前的GRUB(0.9.x)已经被弃用了。而书上使用的0.97版本已经弃用了,GRUB(0.9.x)名称被改为了GRUB Legacy,所以按照书上的操作,我们需要安装GRUB Legacy,不同的Linux安装方式不同,我这里以Ubuntu为例进行说明(如果Linux是运行在虚拟机中,最好做一个快照,这样如果GRUB冲突也好恢复)。安装命令如下:

sudo apt install grub-legacy

完成安装后,可在 /usr/lib/grub/x86_64-pc/ 目录下找到stage1和stage2。将stage1和stage2写入磁盘映像:

dd if=/usr/lib/grub/x86_64-pc/stage1 of=hd.img bs=1 count=446 conv=notrunc
dd if=/usr/lib/grub/x86_64-pc/stage2 of=hd.img bs=512 seek=1 conv=notrunc

也就是说,我们并没有完全安装grub,只是使用它的stage1和stage2,有了它们,多重引导就可以实现了,运行效果如下图所示。

安装了grub的stage1和stage2之后,启动时会出现grub提示符,这时我们输入三个命令:

grup> rootnoverify (hd0,4)
grup> chainloader +1
grup> boot

boot之后敲一个回车,我们的OS就启动起来了。

“rootnoverify”意为将指定分区作为跟分区,但不试图挂载(mount)它。我们知道,grub可以用来启动Linux,并且可以指定启动哪个内核,要做到这一点,grub显然应该是可以识别存放内核文件的文件系统的。这就是root命令试图去挂载文件系统的原因。然后,grub并不认识我们的文件系统——至少目前如此,所以我们用一个rootnoverify来告诉grub,不要试图挂载它,只需要将分区作为根就好了。

需要注意一点,grub对硬盘分区的编号跟Linux下的规则有所不同,它是从零开始编号的。hd[0,1,2,3]表示四个主分区,hd4表示第一个逻辑分区——这是我们的根分区。

“chainloader +1”会把我们刚刚指定的跟分区的引导扇区加载到0x7c00处,也就是说,这一命令完成了之前BIOS完成的工作。

“boot”的作用显而易见,它将控制权交给刚刚读入的引导扇区,于是系统就归我们管了。

小结

我们现在可以从硬盘引导自己的操作系统了,不过由于需要做的工作有点细碎,我们在这里总结一下,将我们的操作系统安装到某个新硬盘的某个新分区需要哪些步骤:

  1. 使用fdisk等工具查看硬盘的分区情况,确定要装到哪个分区,记下分区的首扇区扇区号。
  2. 根据我们在文件系统中提到的规则,算出分区的次设备号。
  3. 将boot/include/load.inc中的ROOT_BASE设置为Orange’S分区的首扇区的扇区号。
  4. 将include/config.h中的MINOR_BOOT设置为Orange’S分区的次设备号。
  5. 如果使用Bochs的话,确认bochsrc中设置了正确的磁盘映像文件。
  6. 用dd等工具将hdboot.bin写入引导扇区。
  7. 将hdldr.bin和kernel.bin打包入cmd.tar。
  8. 将cmd.tar用dd命令写入硬盘(注意位置一定要再三确认,以免破坏硬盘中已有的数据)。
  9. 如果硬盘上没有grub的话,用dd命令写入stage1和stage2。
  10. 从软盘启动,这时mkfs()会将硬盘的相应分区做成Orange’S FS格式,并且会将cmd.tar解开,这时FS中就有hdldr.bin和kernel.bin了。
  11. 从硬盘启动,待出现grub提示符时,输入命令(注意确认rootnoverify的参数),启动成功。

欢迎关注我的公众号


 

公众号中对应文章附有当前文章代码下载说明。

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值