这篇文章是尾声的第二部分,记录如何将Orange’S安装到硬盘上,并实现从硬盘启动。
目录
从硬盘引导
虽然我们的硬盘上已经有不少内容了,但到目前为止,我们的系统始终是从软盘启动的。下面我们要做的事情就是将Orange’S安装到硬盘上,并实现硬盘启动。
我们先回忆一下从软盘启动的过程:
- BIOS将引导扇区读入内存0000:7c00处;
- 跳转到0000:7c00处开始执行引导代码;
- 引导代码从软盘中找到loader.bin,并将其读入内存;
- 跳转到loader.bin开始执行;
- loader.bin从软盘中找到kernel.bin,并将其读入内存;
- 跳转到kernel.bin开始执行,到此可认为启动过程结束;
- 系统运行中。
在第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读取软盘扇区,实际上这个中断也可用来读取硬盘,见下表。
中断号 | 寄存器 | 作用 | |
13h | ah: 02h | al: 要读扇区数 | 从磁盘读数据入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,见下表。
偏移 | 位数 | 描述 |
0 | 8 | Packet有多少字节 |
1 | 8 | 保留 |
2 | 8 | 要传输的块数(最大值为127;0表示不传输数据) |
3 | 8 | 保留 |
4 | 32 | 读操作的目的地址(段:偏移) |
8 | 64 | LBA地址 |
当读磁盘扇区时,只需要将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的文件内容。
此外需要留意以下几个方面:
- ROOT_BASE以宏的方式定义,所以如果更换磁盘或者分区,hdboot.asm需要重新编译。之所以这样做,是因为对于某个特定的系统,这个值是固定的,而且通过宏来“指定”它,而不是通过分区表来“计算”它,将大大简化hdboot.asm的代码。
- 代码中用到了若干的宏,比如SB_INODE_SIZE、SB_NR_IMAP_SECTS等,它们需要和fs.h中super_block结构一致。
- read_sector中,寄存器DL总是被赋值为0x80,也就是说,这个引导扇区默认我们的操作系统安装在第一块硬盘上。
- 并不是所有的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不会出错。
- 跟第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”的作用显而易见,它将控制权交给刚刚读入的引导扇区,于是系统就归我们管了。
小结
我们现在可以从硬盘引导自己的操作系统了,不过由于需要做的工作有点细碎,我们在这里总结一下,将我们的操作系统安装到某个新硬盘的某个新分区需要哪些步骤:
- 使用fdisk等工具查看硬盘的分区情况,确定要装到哪个分区,记下分区的首扇区扇区号。
- 根据我们在文件系统中提到的规则,算出分区的次设备号。
- 将boot/include/load.inc中的ROOT_BASE设置为Orange’S分区的首扇区的扇区号。
- 将include/config.h中的MINOR_BOOT设置为Orange’S分区的次设备号。
- 如果使用Bochs的话,确认bochsrc中设置了正确的磁盘映像文件。
- 用dd等工具将hdboot.bin写入引导扇区。
- 将hdldr.bin和kernel.bin打包入cmd.tar。
- 将cmd.tar用dd命令写入硬盘(注意位置一定要再三确认,以免破坏硬盘中已有的数据)。
- 如果硬盘上没有grub的话,用dd命令写入stage1和stage2。
- 从软盘启动,这时mkfs()会将硬盘的相应分区做成Orange’S FS格式,并且会将cmd.tar解开,这时FS中就有hdldr.bin和kernel.bin了。
- 从硬盘启动,待出现grub提示符时,输入命令(注意确认rootnoverify的参数),启动成功。
欢迎关注我的公众号
公众号中对应文章附有当前文章代码下载说明。